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

{message}

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

{message}

+ ) + + ) +} +``` + +Your broker should accept websocket connection (see [MQTT over Websockets](https://github.com/moscajs/aedes/blob/master/docs/Examples.md#mqtt-server-over-websocket-using-server-factory) to setup [Aedes](https://github.com/moscajs/aedes)). + + +## About QoS + +Here is how QoS works: + +* QoS 0 : received **at most once** : The packet is sent, and that's it. There is no validation about whether it has been received. +* QoS 1 : received **at least once** : The packet is sent and stored as long as the client has not received a confirmation from the server. MQTT ensures that it *will* be received, but there can be duplicates. +* QoS 2 : received **exactly once** : Same as QoS 1 but there is no duplicates. + +About data consumption, obviously, QoS 2 > QoS 1 > QoS 0, if that's a concern to you. + + +## Usage with TypeScript +This repo bundles TypeScript definition files for use in TypeScript projects and to support tools that can read `.d.ts` files. + +### Pre-requisites +Before you can begin using these TypeScript definitions with your project, you need to make sure your project meets a few of these requirements: + * TypeScript >= 2.1 + * Set tsconfig.json: `{"compilerOptions" : {"moduleResolution" : "node"}, ...}` + * Includes the TypeScript definitions for node. You can use npm to install this by typing the following into a terminal window: + `npm install --save-dev @types/node` + + +## Contributing + +MQTT.js is an **OPEN Open Source Project**. This means that: + +> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. + +See the [CONTRIBUTING.md](https://github.com/mqttjs/MQTT.js/blob/master/CONTRIBUTING.md) file for more details. + +### Contributors + +MQTT.js is only possible due to the excellent work of the following contributors: + + + + + + +
Adam RuddGitHub/adamvrTwitter/@adam_vr
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
Maxime AgorGitHub/4rzaelTwitter/@4rzael
Siarhei BuntsevichGitHub/scarry1992
+ + +## License + +MIT diff --git a/benchmarks/bombing.js b/benchmarks/bombing.js index a08fd206b..adef01445 100755 --- a/benchmarks/bombing.js +++ b/benchmarks/bombing.js @@ -1,26 +1,26 @@ -#! /usr/bin/env node - -var mqtt = require('../') -var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) - -var sent = 0 -var interval = 5000 - -function count () { - console.log('sent/s', sent / interval * 1000) - sent = 0 -} - -setInterval(count, interval) - -function publish () { - sent++ - client.publish('test', 'payload', publish) -} - -client.on('connect', publish) - -client.on('error', function () { - console.log('reconnect!') - client.stream.end() -}) +#! /usr/bin/env node + +var mqtt = require('../') +var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, keepalive: 0 }) + +var sent = 0 +var interval = 5000 + +function count () { + console.log('sent/s', sent / interval * 1000) + sent = 0 +} + +setInterval(count, interval) + +function publish () { + sent++ + client.publish('test', 'payload', publish) +} + +client.on('connect', publish) + +client.on('error', function () { + console.log('reconnect!') + client.stream.end() +}) diff --git a/benchmarks/throughputCounter.js b/benchmarks/throughputCounter.js index 90c15fc9d..0b778ef2c 100755 --- a/benchmarks/throughputCounter.js +++ b/benchmarks/throughputCounter.js @@ -1,22 +1,22 @@ -#! /usr/bin/env node - -var mqtt = require('../') - -var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) -var counter = 0 -var interval = 5000 - -function count () { - console.log('received/s', counter / interval * 1000) - counter = 0 -} - -setInterval(count, interval) - -client.on('connect', function () { - count() - this.subscribe('test') - this.on('message', function () { - counter++ - }) -}) +#! /usr/bin/env node + +var mqtt = require('../') + +var client = mqtt.connect({ port: 1883, host: 'localhost', clean: true, encoding: 'binary', keepalive: 0 }) +var counter = 0 +var interval = 5000 + +function count () { + console.log('received/s', counter / interval * 1000) + counter = 0 +} + +setInterval(count, interval) + +client.on('connect', function () { + count() + this.subscribe('test') + this.on('message', function () { + counter++ + }) +}) diff --git a/bin/mqtt.js b/bin/mqtt.js index 4a277306e..022b33a64 100755 --- a/bin/mqtt.js +++ b/bin/mqtt.js @@ -1,27 +1,27 @@ -#!/usr/bin/env node -'use strict' - -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ -var path = require('path') -var commist = require('commist')() -var helpMe = require('help-me')({ - dir: path.join(path.dirname(require.main.filename), '/../doc'), - ext: '.txt' -}) - -commist.register('publish', require('./pub')) -commist.register('subscribe', require('./sub')) -commist.register('version', function () { - console.log('MQTT.js version:', require('./../package.json').version) -}) -commist.register('help', helpMe.toStdout) - -if (commist.parse(process.argv.slice(2)) !== null) { - console.log('No such command:', process.argv[2], '\n') - helpMe.toStdout() -} +#!/usr/bin/env node +'use strict' + +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ +var path = require('path') +var commist = require('commist')() +var helpMe = require('help-me')({ + dir: path.join(path.dirname(require.main.filename), '/../doc'), + ext: '.txt' +}) + +commist.register('publish', require('./pub')) +commist.register('subscribe', require('./sub')) +commist.register('version', function () { + console.log('MQTT.js version:', require('./../package.json').version) +}) +commist.register('help', helpMe.toStdout) + +if (commist.parse(process.argv.slice(2)) !== null) { + console.log('No such command:', process.argv[2], '\n') + helpMe.toStdout() +} diff --git a/bin/pub.js b/bin/pub.js index aefa4b7b6..94b066b40 100755 --- a/bin/pub.js +++ b/bin/pub.js @@ -1,146 +1,146 @@ -#!/usr/bin/env node - -'use strict' - -var mqtt = require('../') -var pump = require('pump') -var path = require('path') -var fs = require('fs') -var concat = require('concat-stream') -var Writable = require('readable-stream').Writable -var helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') -}) -var minimist = require('minimist') -var split2 = require('split2') - -function send (args) { - var client = mqtt.connect(args) - client.on('connect', function () { - client.publish(args.topic, args.message, args, function (err) { - if (err) { - console.warn(err) - } - client.end() - }) - }) - client.on('error', function (err) { - console.warn(err) - client.end() - }) -} - -function multisend (args) { - var client = mqtt.connect(args) - var sender = new Writable({ - objectMode: true - }) - sender._write = function (line, enc, cb) { - client.publish(args.topic, line.trim(), args, cb) - } - - client.on('connect', function () { - pump(process.stdin, split2(), sender, function (err) { - client.end() - if (err) { - throw err - } - }) - }) -} - -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], - boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - message: 'm', - qos: 'q', - clientId: ['i', 'id'], - retain: 'r', - username: 'u', - password: 'P', - stdin: 's', - multiline: 'M', - protocol: ['C', 'l'], - help: 'H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - topic: '', - message: '' - } - }) - - if (args.help) { - return helpMe.toStdout('publish') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - args.topic = (args.topic || args._.shift()).toString() - args.message = (args.message || args._.shift()).toString() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('publish') - } - - if (args.stdin) { - if (args.multiline) { - multisend(args) - } else { - process.stdin.pipe(concat(function (data) { - args.message = data - send(args) - })) - } - } else { - send(args) - } -} - -module.exports = start - -if (require.main === module) { - start(process.argv.slice(2)) -} +#!/usr/bin/env node + +'use strict' + +var mqtt = require('../') +var pump = require('pump') +var path = require('path') +var fs = require('fs') +var concat = require('concat-stream') +var Writable = require('readable-stream').Writable +var helpMe = require('help-me')({ + dir: path.join(__dirname, '..', 'doc') +}) +var minimist = require('minimist') +var split2 = require('split2') + +function send (args) { + var client = mqtt.connect(args) + client.on('connect', function () { + client.publish(args.topic, args.message, args, function (err) { + if (err) { + console.warn(err) + } + client.end() + }) + }) + client.on('error', function (err) { + console.warn(err) + client.end() + }) +} + +function multisend (args) { + var client = mqtt.connect(args) + var sender = new Writable({ + objectMode: true + }) + sender._write = function (line, enc, cb) { + client.publish(args.topic, line.trim(), args, cb) + } + + client.on('connect', function () { + pump(process.stdin, split2(), sender, function (err) { + client.end() + if (err) { + throw err + } + }) + }) +} + +function start (args) { + args = minimist(args, { + string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'message', 'clientId', 'i', 'id'], + boolean: ['stdin', 'retain', 'help', 'insecure', 'multiline'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + message: 'm', + qos: 'q', + clientId: ['i', 'id'], + retain: 'r', + username: 'u', + password: 'P', + stdin: 's', + multiline: 'M', + protocol: ['C', 'l'], + help: 'H', + ca: 'cafile' + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + topic: '', + message: '' + } + }) + + if (args.help) { + return helpMe.toStdout('publish') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + args.topic = (args.topic || args._.shift()).toString() + args.message = (args.message || args._.shift()).toString() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('publish') + } + + if (args.stdin) { + if (args.multiline) { + multisend(args) + } else { + process.stdin.pipe(concat(function (data) { + args.message = data + send(args) + })) + } + } else { + send(args) + } +} + +module.exports = start + +if (require.main === module) { + start(process.argv.slice(2)) +} diff --git a/bin/sub.js b/bin/sub.js index 4c94ceb54..14bc57458 100755 --- a/bin/sub.js +++ b/bin/sub.js @@ -1,123 +1,123 @@ -#!/usr/bin/env node - -var mqtt = require('../') -var path = require('path') -var fs = require('fs') -var helpMe = require('help-me')({ - dir: path.join(__dirname, '..', 'doc') -}) -var minimist = require('minimist') - -function start (args) { - args = minimist(args, { - string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], - boolean: ['stdin', 'help', 'clean', 'insecure'], - alias: { - port: 'p', - hostname: ['h', 'host'], - topic: 't', - qos: 'q', - clean: 'c', - keepalive: 'k', - clientId: ['i', 'id'], - username: 'u', - password: 'P', - protocol: ['C', 'l'], - verbose: 'v', - help: '-H', - ca: 'cafile' - }, - default: { - host: 'localhost', - qos: 0, - retain: false, - clean: true, - keepAlive: 30 // 30 sec - } - }) - - if (args.help) { - return helpMe.toStdout('subscribe') - } - - args.topic = args.topic || args._.shift() - - if (!args.topic) { - console.error('missing topic\n') - return helpMe.toStdout('subscribe') - } - - if (args.key) { - args.key = fs.readFileSync(args.key) - } - - if (args.cert) { - args.cert = fs.readFileSync(args.cert) - } - - if (args.ca) { - args.ca = fs.readFileSync(args.ca) - } - - if (args.key && args.cert && !args.protocol) { - args.protocol = 'mqtts' - } - - if (args.insecure) { - args.rejectUnauthorized = false - } - - if (args.port) { - if (typeof args.port !== 'number') { - console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) - return - } - } - - if (args['will-topic']) { - args.will = {} - args.will.topic = args['will-topic'] - args.will.payload = args['will-message'] - args.will.qos = args['will-qos'] - args.will.retain = args['will-retain'] - } - - args.keepAlive = args['keep-alive'] - - var client = mqtt.connect(args) - - client.on('connect', function () { - client.subscribe(args.topic, { qos: args.qos }, function (err, result) { - if (err) { - console.error(err) - process.exit(1) - } - - result.forEach(function (sub) { - if (sub.qos > 2) { - console.error('subscription negated to', sub.topic, 'with code', sub.qos) - process.exit(1) - } - }) - }) - }) - - client.on('message', function (topic, payload) { - if (args.verbose) { - console.log(topic, payload.toString()) - } else { - console.log(payload.toString()) - } - }) - - client.on('error', function (err) { - console.warn(err) - client.end() - }) -} - -module.exports = start - -if (require.main === module) { - start(process.argv.slice(2)) -} +#!/usr/bin/env node + +var mqtt = require('../') +var path = require('path') +var fs = require('fs') +var helpMe = require('help-me')({ + dir: path.join(__dirname, '..', 'doc') +}) +var minimist = require('minimist') + +function start (args) { + args = minimist(args, { + string: ['hostname', 'username', 'password', 'key', 'cert', 'ca', 'clientId', 'i', 'id'], + boolean: ['stdin', 'help', 'clean', 'insecure'], + alias: { + port: 'p', + hostname: ['h', 'host'], + topic: 't', + qos: 'q', + clean: 'c', + keepalive: 'k', + clientId: ['i', 'id'], + username: 'u', + password: 'P', + protocol: ['C', 'l'], + verbose: 'v', + help: '-H', + ca: 'cafile' + }, + default: { + host: 'localhost', + qos: 0, + retain: false, + clean: true, + keepAlive: 30 // 30 sec + } + }) + + if (args.help) { + return helpMe.toStdout('subscribe') + } + + args.topic = args.topic || args._.shift() + + if (!args.topic) { + console.error('missing topic\n') + return helpMe.toStdout('subscribe') + } + + if (args.key) { + args.key = fs.readFileSync(args.key) + } + + if (args.cert) { + args.cert = fs.readFileSync(args.cert) + } + + if (args.ca) { + args.ca = fs.readFileSync(args.ca) + } + + if (args.key && args.cert && !args.protocol) { + args.protocol = 'mqtts' + } + + if (args.insecure) { + args.rejectUnauthorized = false + } + + if (args.port) { + if (typeof args.port !== 'number') { + console.warn('# Port: number expected, \'%s\' was given.', typeof args.port) + return + } + } + + if (args['will-topic']) { + args.will = {} + args.will.topic = args['will-topic'] + args.will.payload = args['will-message'] + args.will.qos = args['will-qos'] + args.will.retain = args['will-retain'] + } + + args.keepAlive = args['keep-alive'] + + var client = mqtt.connect(args) + + client.on('connect', function () { + client.subscribe(args.topic, { qos: args.qos }, function (err, result) { + if (err) { + console.error(err) + process.exit(1) + } + + result.forEach(function (sub) { + if (sub.qos > 2) { + console.error('subscription negated to', sub.topic, 'with code', sub.qos) + process.exit(1) + } + }) + }) + }) + + client.on('message', function (topic, payload) { + if (args.verbose) { + console.log(topic, payload.toString()) + } else { + console.log(payload.toString()) + } + }) + + client.on('error', function (err) { + console.warn(err) + client.end() + }) +} + +module.exports = start + +if (require.main === module) { + start(process.argv.slice(2)) +} diff --git a/example.js b/example.js index 91b0bfde6..ba14bf949 100644 --- a/example.js +++ b/example.js @@ -1,11 +1,11 @@ -var mqtt = require('./') -var client = mqtt.connect('mqtt://test.mosquitto.org') - -client.subscribe('presence') -client.publish('presence', 'Hello mqtt') - -client.on('message', function (topic, message) { - console.log(message.toString()) -}) - -client.end() +var mqtt = require('./') +var client = mqtt.connect('mqtt://test.mosquitto.org') + +client.subscribe('presence') +client.publish('presence', 'Hello mqtt') + +client.on('message', function (topic, message) { + console.log(message.toString()) +}) + +client.end() diff --git a/examples/client/secure-client.js b/examples/client/secure-client.js index fefe65d73..bf9b6f092 100644 --- a/examples/client/secure-client.js +++ b/examples/client/secure-client.js @@ -1,24 +1,24 @@ -'use strict' - -var mqtt = require('../..') -var path = require('path') -var fs = require('fs') -var KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) -var CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) - -var PORT = 8443 - -var options = { - port: PORT, - key: KEY, - cert: CERT, - rejectUnauthorized: false -} - -var client = mqtt.connect(options) - -client.subscribe('messages') -client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { - console.log(message) -}) +'use strict' + +var mqtt = require('../..') +var path = require('path') +var fs = require('fs') +var KEY = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-key.pem')) +var CERT = fs.readFileSync(path.join(__dirname, '..', '..', 'test', 'helpers', 'tls-cert.pem')) + +var PORT = 8443 + +var options = { + port: PORT, + key: KEY, + cert: CERT, + rejectUnauthorized: false +} + +var client = mqtt.connect(options) + +client.subscribe('messages') +client.publish('messages', 'Current time is: ' + new Date()) +client.on('message', function (topic, message) { + console.log(message) +}) diff --git a/examples/client/simple-both.js b/examples/client/simple-both.js index 58a048465..8e9268b5f 100644 --- a/examples/client/simple-both.js +++ b/examples/client/simple-both.js @@ -1,13 +1,13 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -// or var client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); - -client.subscribe('presence') -client.publish('presence', 'bin hier') -client.on('message', function (topic, message) { - console.log(message) -}) -client.end() +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +// or var client = mqtt.connect({ port: 1883, host: '192.168.1.100', keepalive: 10000}); + +client.subscribe('presence') +client.publish('presence', 'bin hier') +client.on('message', function (topic, message) { + console.log(message) +}) +client.end() diff --git a/examples/client/simple-publish.js b/examples/client/simple-publish.js index 4f8274c4a..a8b0f89b6 100644 --- a/examples/client/simple-publish.js +++ b/examples/client/simple-publish.js @@ -1,7 +1,7 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -client.publish('presence', 'hello!') -client.end() +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +client.publish('presence', 'hello!') +client.end() diff --git a/examples/client/simple-subscribe.js b/examples/client/simple-subscribe.js index f2c6d2c4a..7989b9c22 100644 --- a/examples/client/simple-subscribe.js +++ b/examples/client/simple-subscribe.js @@ -1,9 +1,9 @@ -'use strict' - -var mqtt = require('../..') -var client = mqtt.connect() - -client.subscribe('presence') -client.on('message', function (topic, message) { - console.log(message) -}) +'use strict' + +var mqtt = require('../..') +var client = mqtt.connect() + +client.subscribe('presence') +client.on('message', function (topic, message) { + console.log(message) +}) diff --git a/examples/tls client/mqttclient.js b/examples/tls client/mqttclient.js index d9bb4693a..392fcb39c 100644 --- a/examples/tls client/mqttclient.js +++ b/examples/tls client/mqttclient.js @@ -1,48 +1,48 @@ -'use strict' - -/** ************************** IMPORTANT NOTE *********************************** - - The certificate used on this example has been generated for a host named stark. - So as host we SHOULD use stark if we want the server to be authorized. - For testing this we should add on the computer running this example a line on - the hosts file: - /etc/hosts [UNIX] - OR - \System32\drivers\etc\hosts [Windows] - - The line to add on the file should be as follows: - stark - *******************************************************************************/ - -var mqtt = require('mqtt') -var fs = require('fs') -var path = require('path') -var KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) -var CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) -var TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) - -var PORT = 1883 -var HOST = 'stark' - -var options = { - port: PORT, - host: HOST, - key: KEY, - cert: CERT, - rejectUnauthorized: true, - // The CA list will be used to determine if server is authorized - ca: TRUSTED_CA_LIST, - protocol: 'mqtts' -} - -var client = mqtt.connect(options) - -client.subscribe('messages') -client.publish('messages', 'Current time is: ' + new Date()) -client.on('message', function (topic, message) { - console.log(message) -}) - -client.on('connect', function () { - console.log('Connected') -}) +'use strict' + +/** ************************** IMPORTANT NOTE *********************************** + + The certificate used on this example has been generated for a host named stark. + So as host we SHOULD use stark if we want the server to be authorized. + For testing this we should add on the computer running this example a line on + the hosts file: + /etc/hosts [UNIX] + OR + \System32\drivers\etc\hosts [Windows] + + The line to add on the file should be as follows: + stark + *******************************************************************************/ + +var mqtt = require('mqtt') +var fs = require('fs') +var path = require('path') +var KEY = fs.readFileSync(path.join(__dirname, '/tls-key.pem')) +var CERT = fs.readFileSync(path.join(__dirname, '/tls-cert.pem')) +var TRUSTED_CA_LIST = fs.readFileSync(path.join(__dirname, '/crt.ca.cg.pem')) + +var PORT = 1883 +var HOST = 'stark' + +var options = { + port: PORT, + host: HOST, + key: KEY, + cert: CERT, + rejectUnauthorized: true, + // The CA list will be used to determine if server is authorized + ca: TRUSTED_CA_LIST, + protocol: 'mqtts' +} + +var client = mqtt.connect(options) + +client.subscribe('messages') +client.publish('messages', 'Current time is: ' + new Date()) +client.on('message', function (topic, message) { + console.log(message) +}) + +client.on('connect', function () { + console.log('Connected') +}) diff --git a/examples/ws/client.js b/examples/ws/client.js index 9349c2971..61524d345 100644 --- a/examples/ws/client.js +++ b/examples/ws/client.js @@ -1,53 +1,53 @@ -'use strict' - -var mqtt = require('../../') - -var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) - -// This sample should be run in tandem with the aedes_server.js file. -// Simply run it: -// $ node aedes_server.js -// -// Then run this file in a separate console: -// $ node websocket_sample.js -// -var host = 'ws://localhost:8080' - -var options = { - keepalive: 30, - clientId: clientId, - protocolId: 'MQTT', - protocolVersion: 4, - clean: true, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - will: { - topic: 'WillMsg', - payload: 'Connection Closed abnormally..!', - qos: 0, - retain: false - }, - rejectUnauthorized: false -} - -console.log('connecting mqtt client') -var client = mqtt.connect(host, options) - -client.on('error', function (err) { - console.log(err) - client.end() -}) - -client.on('connect', function () { - console.log('client connected:' + clientId) - client.subscribe('topic', { qos: 0 }) - client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) -}) - -client.on('message', function (topic, message, packet) { - console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) -}) - -client.on('close', function () { - console.log(clientId + ' disconnected') -}) +'use strict' + +var mqtt = require('../../') + +var clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8) + +// This sample should be run in tandem with the aedes_server.js file. +// Simply run it: +// $ node aedes_server.js +// +// Then run this file in a separate console: +// $ node websocket_sample.js +// +var host = 'ws://localhost:8080' + +var options = { + keepalive: 30, + clientId: clientId, + protocolId: 'MQTT', + protocolVersion: 4, + clean: true, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + will: { + topic: 'WillMsg', + payload: 'Connection Closed abnormally..!', + qos: 0, + retain: false + }, + rejectUnauthorized: false +} + +console.log('connecting mqtt client') +var client = mqtt.connect(host, options) + +client.on('error', function (err) { + console.log(err) + client.end() +}) + +client.on('connect', function () { + console.log('client connected:' + clientId) + client.subscribe('topic', { qos: 0 }) + client.publish('topic', 'wss secure connection demo...!', { qos: 0, retain: false }) +}) + +client.on('message', function (topic, message, packet) { + console.log('Received Message:= ' + message.toString() + '\nOn topic:= ' + topic) +}) + +client.on('close', function () { + console.log(clientId + ' disconnected') +}) diff --git a/examples/wss/client_with_proxy.js b/examples/wss/client_with_proxy.js index 657fe3700..4a0d9f3c9 100644 --- a/examples/wss/client_with_proxy.js +++ b/examples/wss/client_with_proxy.js @@ -1,58 +1,58 @@ -'use strict' - -var mqtt = require('mqtt') -var url = require('url') -var HttpsProxyAgent = require('https-proxy-agent') -/* -host: host of the endpoint you want to connect e.g. my.mqqt.host.com -path: path to you endpoint e.g. '/foo/bar/mqtt' -*/ -var endpoint = 'wss://' -/* create proxy agent -proxy: your proxy e.g. proxy.foo.bar.com -port: http proxy port e.g. 8080 -*/ -var proxy = process.env.http_proxy || 'http://:' -var parsed = url.parse(endpoint) -var proxyOpts = url.parse(proxy) -// true for wss -proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true -var agent = new HttpsProxyAgent(proxyOpts) -var wsOptions = { - agent: agent - // other wsOptions - // foo:'bar' -} -var mqttOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - clientId: 'testClient', - wsOptions: wsOptions -} - -var client = mqtt.connect(parsed, mqttOptions) - -client.on('connect', function () { - console.log('connected') -}) - -client.on('error', function (a) { - console.log('error!' + a) -}) - -client.on('offline', function (a) { - console.log('lost connection!' + a) -}) - -client.on('close', function (a) { - console.log('connection closed!' + a) -}) - -client.on('message', function (topic, message) { - console.log(message.toString()) -}) +'use strict' + +var mqtt = require('mqtt') +var url = require('url') +var HttpsProxyAgent = require('https-proxy-agent') +/* +host: host of the endpoint you want to connect e.g. my.mqqt.host.com +path: path to you endpoint e.g. '/foo/bar/mqtt' +*/ +var endpoint = 'wss://' +/* create proxy agent +proxy: your proxy e.g. proxy.foo.bar.com +port: http proxy port e.g. 8080 +*/ +var proxy = process.env.http_proxy || 'http://:' +var parsed = url.parse(endpoint) +var proxyOpts = url.parse(proxy) +// true for wss +proxyOpts.secureEndpoint = parsed.protocol ? parsed.protocol === 'wss:' : true +var agent = new HttpsProxyAgent(proxyOpts) +var wsOptions = { + agent: agent + // other wsOptions + // foo:'bar' +} +var mqttOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + clientId: 'testClient', + wsOptions: wsOptions +} + +var client = mqtt.connect(parsed, mqttOptions) + +client.on('connect', function () { + console.log('connected') +}) + +client.on('error', function (a) { + console.log('error!' + a) +}) + +client.on('offline', function (a) { + console.log('lost connection!' + a) +}) + +client.on('close', function (a) { + console.log('connection closed!' + a) +}) + +client.on('message', function (topic, message) { + console.log(message.toString()) +}) diff --git a/lib/client.js b/lib/client.js index 6eaeb35ac..540a11780 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,1838 +1,1838 @@ -'use strict' - -/** - * Module dependencies - */ -var EventEmitter = require('events').EventEmitter -var Store = require('./store') -var TopicAliasRecv = require('./topic-alias-recv') -var TopicAliasSend = require('./topic-alias-send') -var mqttPacket = require('mqtt-packet') -var DefaultMessageIdProvider = require('./default-message-id-provider') -var Writable = require('readable-stream').Writable -var inherits = require('inherits') -var reInterval = require('reinterval') -var clone = require('rfdc/default') -var validations = require('./validations') -var xtend = require('xtend') -var debug = require('debug')('mqttjs:client') -var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } -var setImmediate = global.setImmediate || function (callback) { - // works in node v0.8 - nextTick(callback) -} -var defaultConnectOptions = { - keepalive: 60, - reschedulePings: true, - protocolId: 'MQTT', - protocolVersion: 4, - reconnectPeriod: 1000, - connectTimeout: 30 * 1000, - clean: true, - resubscribe: true -} - -var socketErrors = [ - 'ECONNREFUSED', - 'EADDRINUSE', - 'ECONNRESET', - 'ENOTFOUND' -] - -// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. - -var errors = { - 0: '', - 1: 'Unacceptable protocol version', - 2: 'Identifier rejected', - 3: 'Server unavailable', - 4: 'Bad username or password', - 5: 'Not authorized', - 16: 'No matching subscribers', - 17: 'No subscription existed', - 128: 'Unspecified error', - 129: 'Malformed Packet', - 130: 'Protocol Error', - 131: 'Implementation specific error', - 132: 'Unsupported Protocol Version', - 133: 'Client Identifier not valid', - 134: 'Bad User Name or Password', - 135: 'Not authorized', - 136: 'Server unavailable', - 137: 'Server busy', - 138: 'Banned', - 139: 'Server shutting down', - 140: 'Bad authentication method', - 141: 'Keep Alive timeout', - 142: 'Session taken over', - 143: 'Topic Filter invalid', - 144: 'Topic Name invalid', - 145: 'Packet identifier in use', - 146: 'Packet Identifier not found', - 147: 'Receive Maximum exceeded', - 148: 'Topic Alias invalid', - 149: 'Packet too large', - 150: 'Message rate too high', - 151: 'Quota exceeded', - 152: 'Administrative action', - 153: 'Payload format invalid', - 154: 'Retain not supported', - 155: 'QoS not supported', - 156: 'Use another server', - 157: 'Server moved', - 158: 'Shared Subscriptions not supported', - 159: 'Connection rate exceeded', - 160: 'Maximum connect time', - 161: 'Subscription Identifiers not supported', - 162: 'Wildcard Subscriptions not supported' -} - -function defaultId () { - return 'mqttjs_' + Math.random().toString(16).substr(2, 8) -} - -function applyTopicAlias (client, packet) { - if (client.options.protocolVersion === 5) { - if (packet.cmd === 'publish') { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - var topic = packet.topic.toString() - if (client.topicAliasSend) { - if (alias) { - if (topic.length !== 0) { - // register topic alias - debug('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) - if (!client.topicAliasSend.put(topic, alias)) { - debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } else { - if (topic.length !== 0) { - if (client.options.autoAssignTopicAlias) { - alias = client.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) - } else { - alias = client.topicAliasSend.getLruAlias() - client.topicAliasSend.put(topic, alias) - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) - } - } else if (client.options.autoUseTopicAlias) { - alias = client.topicAliasSend.getAliasByTopic(topic) - if (alias) { - packet.topic = '' - packet.properties = {...(packet.properties), topicAlias: alias} - debug('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) - } - } - } - } - } else if (alias) { - debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) - return new Error('Sending Topic Alias out of range') - } - } - } -} - -function removeTopicAliasAndRecoverTopicName (client, packet) { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - - var topic = packet.topic.toString() - if (topic.length === 0) { - // restore topic from alias - if (typeof alias === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - topic = client.topicAliasSend.getTopicByAlias(alias) - if (typeof topic === 'undefined') { - return new Error('Unregistered Topic Alias') - } else { - packet.topic = topic - } - } - } - if (alias) { - delete packet.properties.topicAlias - } -} - -function sendPacket (client, packet, cb) { - debug('sendPacket :: packet: %O', packet) - debug('sendPacket :: emitting `packetsend`') - - client.emit('packetsend', packet) - - debug('sendPacket :: writing to stream') - var result = mqttPacket.writeToStream(packet, client.stream, client.options) - debug('sendPacket :: writeToStream result %s', result) - if (!result && cb) { - debug('sendPacket :: handle events on `drain` once through callback.') - client.stream.once('drain', cb) - } else if (cb) { - debug('sendPacket :: invoking cb') - cb() - } -} - -function flush (queue) { - if (queue) { - debug('flush: queue exists? %b', !!(queue)) - Object.keys(queue).forEach(function (messageId) { - if (typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } -} - -function flushVolatile (queue) { - if (queue) { - debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') - Object.keys(queue).forEach(function (messageId) { - if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { - queue[messageId].cb(new Error('Connection closed')) - delete queue[messageId] - } - }) - } -} - -function storeAndSend (client, packet, cb, cbStorePut) { - debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) - var storePacket = packet - var err - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - err = removeTopicAliasAndRecoverTopicName(client, storePacket) - if (err) { - return cb && cb(err) - } - } - client.outgoingStore.put(storePacket, function storedPacket (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - sendPacket(client, packet, cb) - }) -} - -function nop (error) { - debug('nop ::', error) -} - -/** - * MqttClient constructor - * - * @param {Stream} stream - stream - * @param {Object} [options] - connection options - * (see Connection#connect) - */ -function MqttClient (streamBuilder, options) { - var k - var that = this - - if (!(this instanceof MqttClient)) { - return new MqttClient(streamBuilder, options) - } - - this.options = options || {} - - // Defaults - for (k in defaultConnectOptions) { - if (typeof this.options[k] === 'undefined') { - this.options[k] = defaultConnectOptions[k] - } else { - this.options[k] = options[k] - } - } - - debug('MqttClient :: options.protocol', options.protocol) - debug('MqttClient :: options.protocolVersion', options.protocolVersion) - debug('MqttClient :: options.username', options.username) - debug('MqttClient :: options.keepalive', options.keepalive) - debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) - debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) - debug('MqttClient :: options.topicAliasMaximum', options.topicAliasMaximum) - - this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() - - debug('MqttClient :: clientId', this.options.clientId) - - this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } - - this.streamBuilder = streamBuilder - - this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider - - // Inflight message storages - this.outgoingStore = options.outgoingStore || new Store() - this.incomingStore = options.incomingStore || new Store() - - // Should QoS zero messages be queued when the connection is broken? - this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero - - // map of subscribed topics to support reconnection - this._resubscribeTopics = {} - - // map of a subscribe messageId and a topic - this.messageIdToTopic = {} - - // Ping timer, setup in _setupPingTimer - this.pingTimer = null - // Is the client connected? - this.connected = false - // Are we disconnecting? - this.disconnecting = false - // Packet queue - this.queue = [] - // connack timer - this.connackTimer = null - // Reconnect timer - this.reconnectTimer = null - // Is processing store? - this._storeProcessing = false - // Packet Ids are put into the store during store processing - this._packetIdsDuringStoreProcessing = {} - // Store processing queue - this._storeProcessingQueue = [] - - // Inflight callbacks - this.outgoing = {} - - // True if connection is first time. - this._firstConnection = true - - if (options.topicAliasMaximum > 0) { - if (options.topicAliasMaximum > 0xffff) { - debug('MqttClient :: options.topicAliasMaximum is out of range') - } else { - this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) - } - } - - // Send queued packets - this.on('connect', function () { - var queue = this.queue - - function deliver () { - var entry = queue.shift() - debug('deliver :: entry %o', entry) - var packet = null - - if (!entry) { - that._resubscribe() - return - } - - packet = entry.packet - debug('deliver :: call _sendPacket for %o', packet) - var send = true - if (packet.messageId && packet.messageId !== 0) { - if (!that.messageIdProvider.register(packet.messageId)) { - send = false - } - } - if (send) { - that._sendPacket( - packet, - function (err) { - if (entry.cb) { - entry.cb(err) - } - deliver() - } - ) - } else { - debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) - deliver() - } - } - - debug('connect :: sending queued packets') - deliver() - }) - - this.on('close', function () { - debug('close :: connected set to `false`') - this.connected = false - - debug('close :: clearing connackTimer') - clearTimeout(this.connackTimer) - - debug('close :: clearing ping timer') - if (that.pingTimer !== null) { - that.pingTimer.clear() - that.pingTimer = null - } - - if (this.topicAliasRecv) { - this.topicAliasRecv.clear() - } - - debug('close :: calling _setupReconnect') - this._setupReconnect() - }) - EventEmitter.call(this) - - debug('MqttClient :: setting up stream') - this._setupStream() -} -inherits(MqttClient, EventEmitter) - -/** - * setup the event handlers in the inner stream. - * - * @api private - */ -MqttClient.prototype._setupStream = function () { - var connectPacket - var that = this - var writable = new Writable() - var parser = mqttPacket.parser(this.options) - var completeParse = null - var packets = [] - - debug('_setupStream :: calling method to clear reconnect') - this._clearReconnect() - - debug('_setupStream :: using streamBuilder provided to client to create stream') - this.stream = this.streamBuilder(this) - - parser.on('packet', function (packet) { - debug('parser :: on packet push to packets array.') - packets.push(packet) - }) - - function nextTickWork () { - if (packets.length) { - nextTick(work) - } else { - var done = completeParse - completeParse = null - done() - } - } - - function work () { - debug('work :: getting next packet in queue') - var packet = packets.shift() - - if (packet) { - debug('work :: packet pulled from queue') - that._handlePacket(packet, nextTickWork) - } else { - debug('work :: no packets in queue') - var done = completeParse - completeParse = null - debug('work :: done flag is %s', !!(done)) - if (done) done() - } - } - - writable._write = function (buf, enc, done) { - completeParse = done - debug('writable stream :: parsing buffer') - parser.parse(buf) - work() - } - - function streamErrorHandler (error) { - debug('streamErrorHandler :: error', error.message) - if (socketErrors.includes(error.code)) { - // handle error - debug('streamErrorHandler :: emitting error') - that.emit('error', error) - } else { - nop(error) - } - } - - debug('_setupStream :: pipe stream to writable stream') - this.stream.pipe(writable) - - // Suppress connection errors - this.stream.on('error', streamErrorHandler) - - // Echo stream close - this.stream.on('close', function () { - debug('(%s)stream :: on close', that.options.clientId) - flushVolatile(that.outgoing) - debug('stream: emit close to MqttClient') - that.emit('close') - }) - - // Send a connect packet - debug('_setupStream: sending packet `connect`') - connectPacket = Object.create(this.options) - connectPacket.cmd = 'connect' - if (this.topicAliasRecv) { - if (!connectPacket.properties) { - connectPacket.properties = {} - } - if (this.topicAliasRecv) { - connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max - } - } - // avoid message queue - sendPacket(this, connectPacket) - - // Echo connection errors - parser.on('error', this.emit.bind(this, 'error')) - - // auth - if (this.options.properties) { - if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { - that.end(() => - this.emit('error', new Error('Packet has no Authentication Method') - )) - return this - } - if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { - var authPacket = xtend({cmd: 'auth', reasonCode: 0}, this.options.authPacket) - sendPacket(this, authPacket) - } - } - - // many drain listeners are needed for qos 1 callbacks if the connection is intermittent - this.stream.setMaxListeners(1000) - - clearTimeout(this.connackTimer) - this.connackTimer = setTimeout(function () { - debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') - that._cleanUp(true) - }, this.options.connectTimeout) -} - -MqttClient.prototype._handlePacket = function (packet, done) { - var options = this.options - - if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { - this.emit('error', new Error('exceeding packets size ' + packet.cmd)) - this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) - return this - } - debug('_handlePacket :: emitting packetreceive') - this.emit('packetreceive', packet) - - switch (packet.cmd) { - case 'publish': - this._handlePublish(packet, done) - break - case 'puback': - case 'pubrec': - case 'pubcomp': - case 'suback': - case 'unsuback': - this._handleAck(packet) - done() - break - case 'pubrel': - this._handlePubrel(packet, done) - break - case 'connack': - this._handleConnack(packet) - done() - break - case 'pingresp': - this._handlePingresp(packet) - done() - break - case 'disconnect': - this._handleDisconnect(packet) - done() - break - default: - // do nothing - // maybe we should do an error handling - // or just log it - break - } -} - -MqttClient.prototype._checkDisconnecting = function (callback) { - if (this.disconnecting) { - if (callback) { - callback(new Error('client disconnecting')) - } else { - this.emit('error', new Error('client disconnecting')) - } - } - return this.disconnecting -} - -/** - * publish - publish to - * - * @param {String} topic - topic to publish to - * @param {String, Buffer} message - message to publish - * @param {Object} [opts] - publish options, includes: - * {Number} qos - qos level to publish on - * {Boolean} retain - whether or not to retain the message - * {Boolean} dup - whether or not mark a message as duplicate - * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` - * @param {Function} [callback] - function(err){} - * called when publish succeeds or fails - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.publish('topic', 'message'); - * @example - * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); - * @example client.publish('topic', 'message', console.log); - */ -MqttClient.prototype.publish = function (topic, message, opts, callback) { - debug('publish :: message `%s` to topic `%s`', message, topic) - var packet - var options = this.options - - // .publish(topic, payload, cb); - if (typeof opts === 'function') { - callback = opts - opts = null - } - - // default opts - var defaultOpts = {qos: 0, retain: false, dup: false} - opts = xtend(defaultOpts, opts) - - if (this._checkDisconnecting(callback)) { - return this - } - - var that = this - var publishProc = function () { - var messageId = 0 - if (opts.qos === 1 || opts.qos === 2) { - messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - } - packet = { - cmd: 'publish', - topic: topic, - payload: message, - qos: opts.qos, - retain: opts.retain, - messageId: messageId, - dup: opts.dup - } - - if (options.protocolVersion === 5) { - packet.properties = opts.properties - } - - debug('publish :: qos', opts.qos) - switch (opts.qos) { - case 1: - case 2: - // Add to callbacks - that.outgoing[packet.messageId] = { - volatile: false, - cb: callback || nop - } - debug('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, undefined, opts.cbStorePut) - break - default: - debug('MqttClient:publish: packet cmd: %s', packet.cmd) - that._sendPacket(packet, callback, opts.cbStorePut) - break - } - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': publishProc, - 'cbStorePut': opts.cbStorePut, - 'callback': callback - } - ) - } else { - publishProc() - } - return this -} - -/** - * subscribe - subscribe to - * - * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} - * @param {Object} [opts] - optional subscription options, includes: - * {Number} qos - subscribe qos level - * @param {Function} [callback] - function(err, granted){} where: - * {Error} err - subscription error (none at the moment!) - * {Array} granted - array of {topic: 't', qos: 0} - * @returns {MqttClient} this - for chaining - * @api public - * @example client.subscribe('topic'); - * @example client.subscribe('topic', {qos: 1}); - * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); - * @example client.subscribe('topic', console.log); - */ -MqttClient.prototype.subscribe = function () { - var that = this - var args = new Array(arguments.length) - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - var subs = [] - var obj = args.shift() - var resubscribe = obj.resubscribe - var callback = args.pop() || nop - var opts = args.pop() - var version = this.options.protocolVersion - - delete obj.resubscribe - - if (typeof obj === 'string') { - obj = [obj] - } - - if (typeof callback !== 'function') { - opts = callback - callback = nop - } - - var invalidTopic = validations.validateTopics(obj) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (this._checkDisconnecting(callback)) { - debug('subscribe: discconecting true') - return this - } - - var defaultOpts = { - qos: 0 - } - if (version === 5) { - defaultOpts.nl = false - defaultOpts.rap = false - defaultOpts.rh = 0 - } - opts = xtend(defaultOpts, opts) - - if (Array.isArray(obj)) { - obj.forEach(function (topic) { - debug('subscribe: array topic %s', topic) - if (!that._resubscribeTopics.hasOwnProperty(topic) || - that._resubscribeTopics[topic].qos < opts.qos || - resubscribe) { - var currentOpts = { - topic: topic, - qos: opts.qos - } - if (version === 5) { - currentOpts.nl = opts.nl - currentOpts.rap = opts.rap - currentOpts.rh = opts.rh - currentOpts.properties = opts.properties - } - debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) - subs.push(currentOpts) - } - }) - } else { - Object - .keys(obj) - .forEach(function (k) { - debug('subscribe: object topic %s', k) - if (!that._resubscribeTopics.hasOwnProperty(k) || - that._resubscribeTopics[k].qos < obj[k].qos || - resubscribe) { - var currentOpts = { - topic: k, - qos: obj[k].qos - } - if (version === 5) { - currentOpts.nl = obj[k].nl - currentOpts.rap = obj[k].rap - currentOpts.rh = obj[k].rh - currentOpts.properties = opts.properties - } - debug('subscribe: pushing `%s` to subs list', currentOpts) - subs.push(currentOpts) - } - }) - } - - if (!subs.length) { - callback(null, []) - return this - } - - var subscribeProc = function () { - var messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - - var packet = { - cmd: 'subscribe', - subscriptions: subs, - qos: 1, - retain: false, - dup: false, - messageId: messageId - } - - if (opts.properties) { - packet.properties = opts.properties - } - - // subscriptions to resubscribe to in case of disconnect - if (that.options.resubscribe) { - debug('subscribe :: resubscribe true') - var topics = [] - subs.forEach(function (sub) { - if (that.options.reconnectPeriod > 0) { - var topic = { qos: sub.qos } - if (version === 5) { - topic.nl = sub.nl || false - topic.rap = sub.rap || false - topic.rh = sub.rh || 0 - topic.properties = sub.properties - } - that._resubscribeTopics[sub.topic] = topic - topics.push(sub.topic) - } - }) - that.messageIdToTopic[packet.messageId] = topics - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: function (err, packet) { - if (!err) { - var granted = packet.granted - for (var i = 0; i < granted.length; i += 1) { - subs[i].qos = granted[i] - } - } - - callback(err, subs) - } - } - debug('subscribe :: call _sendPacket') - that._sendPacket(packet) - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': subscribeProc, - 'callback': callback - } - ) - } else { - subscribeProc() - } - - return this -} - -/** - * unsubscribe - unsubscribe from topic(s) - * - * @param {String, Array} topic - topics to unsubscribe from - * @param {Object} [opts] - optional subscription options, includes: - * {Object} properties - properties of unsubscribe packet - * @param {Function} [callback] - callback fired on unsuback - * @returns {MqttClient} this - for chaining - * @api public - * @example client.unsubscribe('topic'); - * @example client.unsubscribe('topic', console.log); - */ -MqttClient.prototype.unsubscribe = function () { - var that = this - var args = new Array(arguments.length) - for (var i = 0; i < arguments.length; i++) { - args[i] = arguments[i] - } - var topic = args.shift() - var callback = args.pop() || nop - var opts = args.pop() - if (typeof topic === 'string') { - topic = [topic] - } - - if (typeof callback !== 'function') { - opts = callback - callback = nop - } - - var invalidTopic = validations.validateTopics(topic) - if (invalidTopic !== null) { - setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) - return this - } - - if (that._checkDisconnecting(callback)) { - return this - } - - var unsubscribeProc = function () { - var messageId = that._nextId() - if (messageId === null) { - debug('No messageId left') - return false - } - var packet = { - cmd: 'unsubscribe', - qos: 1, - messageId: messageId - } - - if (typeof topic === 'string') { - packet.unsubscriptions = [topic] - } else if (Array.isArray(topic)) { - packet.unsubscriptions = topic - } - - if (that.options.resubscribe) { - packet.unsubscriptions.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - - if (typeof opts === 'object' && opts.properties) { - packet.properties = opts.properties - } - - that.outgoing[packet.messageId] = { - volatile: true, - cb: callback - } - - debug('unsubscribe: call _sendPacket') - that._sendPacket(packet) - - return true - } - - if (this._storeProcessing || this._storeProcessingQueue.length > 0) { - this._storeProcessingQueue.push( - { - 'invoke': unsubscribeProc, - 'callback': callback - } - ) - } else { - unsubscribeProc() - } - - return this -} - -/** - * end - close connection - * - * @returns {MqttClient} this - for chaining - * @param {Boolean} force - do not wait for all in-flight messages to be acked - * @param {Object} opts - added to the disconnect packet - * @param {Function} cb - called when the client has been closed - * - * @api public - */ -MqttClient.prototype.end = function (force, opts, cb) { - var that = this - - debug('end :: (%s)', this.options.clientId) - - if (force == null || typeof force !== 'boolean') { - cb = opts || nop - opts = force - force = false - if (typeof opts !== 'object') { - cb = opts - opts = null - if (typeof cb !== 'function') { - cb = nop - } - } - } - - if (typeof opts !== 'object') { - cb = opts - opts = null - } - - debug('end :: cb? %s', !!cb) - cb = cb || nop - - function closeStores () { - debug('end :: closeStores: closing incoming and outgoing stores') - that.disconnected = true - that.incomingStore.close(function (e1) { - that.outgoingStore.close(function (e2) { - debug('end :: closeStores: emitting end') - that.emit('end') - if (cb) { - let err = e1 || e2 - debug('end :: closeStores: invoking callback with args') - cb(err) - } - }) - }) - if (that._deferredReconnect) { - that._deferredReconnect() - } - } - - function finish () { - // defer closesStores of an I/O cycle, - // just to make sure things are - // ok for websockets - debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) - that._cleanUp(force, () => { - debug('end :: finish :: calling process.nextTick on closeStores') - // var boundProcess = nextTick.bind(null, closeStores) - nextTick(closeStores.bind(that)) - }, opts) - } - - if (this.disconnecting) { - cb() - return this - } - - this._clearReconnect() - - this.disconnecting = true - - if (!force && Object.keys(this.outgoing).length > 0) { - // wait 10ms, just to be sure we received all of it - debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) - this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) - } else { - debug('end :: (%s) :: immediately calling finish', that.options.clientId) - finish() - } - - return this -} - -/** - * removeOutgoingMessage - remove a message in outgoing store - * the outgoing callback will be called withe Error('Message removed') if the message is removed - * - * @param {Number} messageId - messageId to remove message - * @returns {MqttClient} this - for chaining - * @api public - * - * @example client.removeOutgoingMessage(client.getLastAllocated()); - */ -MqttClient.prototype.removeOutgoingMessage = function (messageId) { - var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null - delete this.outgoing[messageId] - this.outgoingStore.del({messageId: messageId}, function () { - cb(new Error('Message removed')) - }) - return this -} - -/** - * reconnect - connect again using the same options as connect() - * - * @param {Object} [opts] - optional reconnect options, includes: - * {Store} incomingStore - a store for the incoming packets - * {Store} outgoingStore - a store for the outgoing packets - * if opts is not given, current stores are used - * @returns {MqttClient} this - for chaining - * - * @api public - */ -MqttClient.prototype.reconnect = function (opts) { - debug('client reconnect') - var that = this - var f = function () { - if (opts) { - that.options.incomingStore = opts.incomingStore - that.options.outgoingStore = opts.outgoingStore - } else { - that.options.incomingStore = null - that.options.outgoingStore = null - } - that.incomingStore = that.options.incomingStore || new Store() - that.outgoingStore = that.options.outgoingStore || new Store() - that.disconnecting = false - that.disconnected = false - that._deferredReconnect = null - that._reconnect() - } - - if (this.disconnecting && !this.disconnected) { - this._deferredReconnect = f - } else { - f() - } - return this -} - -/** - * _reconnect - implement reconnection - * @api privateish - */ -MqttClient.prototype._reconnect = function () { - debug('_reconnect: emitting reconnect to client') - this.emit('reconnect') - if (this.connected) { - this.end(() => { this._setupStream() }) - debug('client already connected. disconnecting first.') - } else { - debug('_reconnect: calling _setupStream') - this._setupStream() - } -} - -/** - * _setupReconnect - setup reconnect timer - */ -MqttClient.prototype._setupReconnect = function () { - var that = this - - if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { - if (!this.reconnecting) { - debug('_setupReconnect :: emit `offline` state') - this.emit('offline') - debug('_setupReconnect :: set `reconnecting` to `true`') - this.reconnecting = true - } - debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) - that.reconnectTimer = setInterval(function () { - debug('reconnectTimer :: reconnect triggered!') - that._reconnect() - }, that.options.reconnectPeriod) - } else { - debug('_setupReconnect :: doing nothing...') - } -} - -/** - * _clearReconnect - clear the reconnect timer - */ -MqttClient.prototype._clearReconnect = function () { - debug('_clearReconnect : clearing reconnect timer') - if (this.reconnectTimer) { - clearInterval(this.reconnectTimer) - this.reconnectTimer = null - } -} - -/** - * _cleanUp - clean up on connection end - * @api private - */ -MqttClient.prototype._cleanUp = function (forced, done) { - var opts = arguments[2] - if (done) { - debug('_cleanUp :: done callback provided for on stream close') - this.stream.on('close', done) - } - - debug('_cleanUp :: forced? %s', forced) - if (forced) { - if ((this.options.reconnectPeriod === 0) && this.options.clean) { - flush(this.outgoing) - } - debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) - this.stream.destroy() - } else { - var packet = xtend({ cmd: 'disconnect' }, opts) - debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) - this._sendPacket( - packet, - setImmediate.bind( - null, - this.stream.end.bind(this.stream) - ) - ) - } - - if (!this.disconnecting) { - debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') - this._clearReconnect() - this._setupReconnect() - } - - if (this.pingTimer !== null) { - debug('_cleanUp :: clearing pingTimer') - this.pingTimer.clear() - this.pingTimer = null - } - - if (done && !this.connected) { - debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) - this.stream.removeListener('close', done) - done() - } -} - -/** - * _sendPacket - send or queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ -MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { - debug('_sendPacket :: (%s) :: start', this.options.clientId) - cbStorePut = cbStorePut || nop - cb = cb || nop - - var err = applyTopicAlias(this, packet) - if (err) { - cb(err) - return - } - - if (!this.connected) { - debug('_sendPacket :: client not connected. Storing packet offline.') - this._storePacket(packet, cb, cbStorePut) - return - } - - // When sending a packet, reschedule the ping timer - this._shiftPingInterval() - - switch (packet.cmd) { - case 'publish': - break - case 'pubrel': - storeAndSend(this, packet, cb, cbStorePut) - return - default: - sendPacket(this, packet, cb) - return - } - - switch (packet.qos) { - case 2: - case 1: - storeAndSend(this, packet, cb, cbStorePut) - break - /** - * no need of case here since it will be caught by default - * and jshint comply that before default it must be a break - * anyway it will result in -1 evaluation - */ - case 0: - /* falls through */ - default: - sendPacket(this, packet, cb) - break - } - debug('_sendPacket :: (%s) :: end', this.options.clientId) -} - -/** - * _storePacket - queue a packet - * @param {Object} packet - packet options - * @param {Function} cb - callback when the packet is sent - * @param {Function} cbStorePut - called when message is put into outgoingStore - * @api private - */ -MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { - debug('_storePacket :: packet: %o', packet) - debug('_storePacket :: cb? %s', !!cb) - cbStorePut = cbStorePut || nop - - var storePacket = packet - if (storePacket.cmd === 'publish') { - // The original packet is for sending. - // The cloned storePacket is for storing to resend on reconnect. - // Topic Alias must not be used after disconnected. - storePacket = clone(packet) - var err = removeTopicAliasAndRecoverTopicName(this, storePacket) - if (err) { - return cb && cb(err) - } - } - // check that the packet is not a qos of 0, or that the command is not a publish - if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { - this.queue.push({ packet: storePacket, cb: cb }) - } else if (storePacket.qos > 0) { - cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null - this.outgoingStore.put(storePacket, function (err) { - if (err) { - return cb && cb(err) - } - cbStorePut() - }) - } else if (cb) { - cb(new Error('No connection to broker')) - } -} - -/** - * _setupPingTimer - setup the ping timer - * - * @api private - */ -MqttClient.prototype._setupPingTimer = function () { - debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) - var that = this - - if (!this.pingTimer && this.options.keepalive) { - this.pingResp = true - this.pingTimer = reInterval(function () { - that._checkPing() - }, this.options.keepalive * 1000) - } -} - -/** - * _shiftPingInterval - reschedule the ping interval - * - * @api private - */ -MqttClient.prototype._shiftPingInterval = function () { - if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { - this.pingTimer.reschedule(this.options.keepalive * 1000) - } -} -/** - * _checkPing - check if a pingresp has come back, and ping the server again - * - * @api private - */ -MqttClient.prototype._checkPing = function () { - debug('_checkPing :: checking ping...') - if (this.pingResp) { - debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') - this.pingResp = false - this._sendPacket({ cmd: 'pingreq' }) - } else { - // do a forced cleanup since socket will be in bad shape - debug('_checkPing :: calling _cleanUp with force true') - this._cleanUp(true) - } -} - -/** - * _handlePingresp - handle a pingresp - * - * @api private - */ -MqttClient.prototype._handlePingresp = function () { - this.pingResp = true -} - -/** - * _handleConnack - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handleConnack = function (packet) { - debug('_handleConnack') - var options = this.options - var version = options.protocolVersion - var rc = version === 5 ? packet.reasonCode : packet.returnCode - - clearTimeout(this.connackTimer) - delete this.topicAliasSend - - if (packet.properties) { - if (packet.properties.topicAliasMaximum) { - if (packet.properties.topicAliasMaximum > 0xffff) { - this.emit('error', new Error('topicAliasMaximum from broker is out of range')) - return - } - if (packet.properties.topicAliasMaximum > 0) { - this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) - } - } - if (packet.properties.serverKeepAlive && options.keepalive) { - options.keepalive = packet.properties.serverKeepAlive - this._shiftPingInterval() - } - if (packet.properties.maximumPacketSize) { - if (!options.properties) { options.properties = {} } - options.properties.maximumPacketSize = packet.properties.maximumPacketSize - } - } - - if (rc === 0) { - this.reconnecting = false - this._onConnect(packet) - } else if (rc > 0) { - var err = new Error('Connection refused: ' + errors[rc]) - err.code = rc - this.emit('error', err) - } -} - -/** - * _handlePublish - * - * @param {Object} packet - * @api private - */ -/* -those late 2 case should be rewrite to comply with coding style: - -case 1: -case 0: - // do not wait sending a puback - // no callback passed - if (1 === qos) { - this._sendPacket({ - cmd: 'puback', - messageId: messageId - }); - } - // emit the message event for both qos 1 and 0 - this.emit('message', topic, message, packet); - this.handleMessage(packet, done); - break; -default: - // do nothing but every switch mus have a default - // log or throw an error about unknown qos - break; - -for now i just suppressed the warnings -*/ -MqttClient.prototype._handlePublish = function (packet, done) { - debug('_handlePublish: packet %o', packet) - done = typeof done !== 'undefined' ? done : nop - var topic = packet.topic.toString() - var message = packet.payload - var qos = packet.qos - var messageId = packet.messageId - var that = this - var options = this.options - var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] - if (this.options.protocolVersion === 5) { - var alias - if (packet.properties) { - alias = packet.properties.topicAlias - } - if (typeof alias !== 'undefined') { - if (topic.length === 0) { - if (alias > 0 && alias <= 0xffff) { - var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) - if (gotTopic) { - topic = gotTopic - debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) - } else { - debug('_handlePublish :: unregistered topic alias. alias: %d', alias) - this.emit('error', new Error('Received unregistered Topic Alias')) - return - } - } else { - debug('_handlePublish :: topic alias out of range. alias: %d', alias) - this.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } else { - if (this.topicAliasRecv.put(topic, alias)) { - debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) - } else { - debug('_handlePublish :: topic alias out of range. alias: %d', alias) - this.emit('error', new Error('Received Topic Alias is out of range')) - return - } - } - } - } - debug('_handlePublish: qos %d', qos) - switch (qos) { - case 2: { - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return that.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } - if (code) { - that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) - } else { - that.incomingStore.put(packet, function () { - that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) - }) - } - }) - break - } - case 1: { - // emit the message event - options.customHandleAcks(topic, message, packet, function (error, code) { - if (!(error instanceof Error)) { - code = error - error = null - } - if (error) { return that.emit('error', error) } - if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for puback')) } - if (!code) { that.emit('message', topic, message, packet) } - that.handleMessage(packet, function (err) { - if (err) { - return done && done(err) - } - that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) - }) - }) - break - } - case 0: - // emit the message event - this.emit('message', topic, message, packet) - this.handleMessage(packet, done) - break - default: - // do nothing - debug('_handlePublish: unknown QoS. Doing nothing.') - // log or throw an error about unknown qos - break - } -} - -/** - * Handle messages with backpressure support, one at a time. - * Override at will. - * - * @param Packet packet the packet - * @param Function callback call when finished - * @api public - */ -MqttClient.prototype.handleMessage = function (packet, callback) { - callback() -} - -/** - * _handleAck - * - * @param {Object} packet - * @api private - */ - -MqttClient.prototype._handleAck = function (packet) { - /* eslint no-fallthrough: "off" */ - var messageId = packet.messageId - var type = packet.cmd - var response = null - var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null - var that = this - var err - - if (!cb) { - debug('_handleAck :: Server sent an ack in error. Ignoring.') - // Server sent an ack in error, ignore it. - return - } - - // Process - debug('_handleAck :: packet type', type) - switch (type) { - case 'pubcomp': - // same thing as puback for QoS 2 - case 'puback': - var pubackRC = packet.reasonCode - // Callback - we're done - if (pubackRC && pubackRC > 0 && pubackRC !== 16) { - err = new Error('Publish error: ' + errors[pubackRC]) - err.code = pubackRC - cb(err, packet) - } - delete this.outgoing[messageId] - this.outgoingStore.del(packet, cb) - this.messageIdProvider.deallocate(messageId) - this._invokeStoreProcessingQueue() - break - case 'pubrec': - response = { - cmd: 'pubrel', - qos: 2, - messageId: messageId - } - var pubrecRC = packet.reasonCode - - if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { - err = new Error('Publish error: ' + errors[pubrecRC]) - err.code = pubrecRC - cb(err, packet) - } else { - this._sendPacket(response) - } - break - case 'suback': - delete this.outgoing[messageId] - this.messageIdProvider.deallocate(messageId) - for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { - if ((packet.granted[grantedI] & 0x80) !== 0) { - // suback with Failure status - var topics = this.messageIdToTopic[messageId] - if (topics) { - topics.forEach(function (topic) { - delete that._resubscribeTopics[topic] - }) - } - } - } - this._invokeStoreProcessingQueue() - cb(null, packet) - break - case 'unsuback': - delete this.outgoing[messageId] - this.messageIdProvider.deallocate(messageId) - this._invokeStoreProcessingQueue() - cb(null) - break - default: - that.emit('error', new Error('unrecognized packet type')) - } - - if (this.disconnecting && - Object.keys(this.outgoing).length === 0) { - this.emit('outgoingEmpty') - } -} - -/** - * _handlePubrel - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handlePubrel = function (packet, callback) { - debug('handling pubrel packet') - callback = typeof callback !== 'undefined' ? callback : nop - var messageId = packet.messageId - var that = this - - var comp = {cmd: 'pubcomp', messageId: messageId} - - that.incomingStore.get(packet, function (err, pub) { - if (!err) { - that.emit('message', pub.topic, pub.payload, pub) - that.handleMessage(pub, function (err) { - if (err) { - return callback(err) - } - that.incomingStore.del(pub, nop) - that._sendPacket(comp, callback) - }) - } else { - that._sendPacket(comp, callback) - } - }) -} - -/** - * _handleDisconnect - * - * @param {Object} packet - * @api private - */ -MqttClient.prototype._handleDisconnect = function (packet) { - this.emit('disconnect', packet) -} - -/** - * _nextId - * @return unsigned int - */ -MqttClient.prototype._nextId = function () { - return this.messageIdProvider.allocate() -} - -/** - * getLastMessageId - * @return unsigned int - */ -MqttClient.prototype.getLastMessageId = function () { - return this.messageIdProvider.getLastAllocated() -} - -/** - * _resubscribe - * @api private - */ -MqttClient.prototype._resubscribe = function () { - debug('_resubscribe') - var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) - if (!this._firstConnection && - (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && - _resubscribeTopicsKeys.length > 0) { - if (this.options.resubscribe) { - if (this.options.protocolVersion === 5) { - debug('_resubscribe: protocolVersion 5') - for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { - var resubscribeTopic = {} - resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] - resubscribeTopic.resubscribe = true - this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) - } - } else { - this._resubscribeTopics.resubscribe = true - this.subscribe(this._resubscribeTopics) - } - } else { - this._resubscribeTopics = {} - } - } - - this._firstConnection = false -} - -/** - * _onConnect - * - * @api private - */ -MqttClient.prototype._onConnect = function (packet) { - if (this.disconnected) { - this.emit('connect', packet) - return - } - - var that = this - - this.connackPacket = packet - this.messageIdProvider.clear() - this._setupPingTimer() - - this.connected = true - - function startStreamProcess () { - var outStore = that.outgoingStore.createStream() - - function clearStoreProcessing () { - that._storeProcessing = false - that._packetIdsDuringStoreProcessing = {} - } - - that.once('close', remove) - outStore.on('error', function (err) { - clearStoreProcessing() - that._flushStoreProcessingQueue() - that.removeListener('close', remove) - that.emit('error', err) - }) - - function remove () { - outStore.destroy() - outStore = null - that._flushStoreProcessingQueue() - clearStoreProcessing() - } - - function storeDeliver () { - // edge case, we wrapped this twice - if (!outStore) { - return - } - that._storeProcessing = true - - var packet = outStore.read(1) - - var cb - - if (!packet) { - // read when data is available in the future - outStore.once('readable', storeDeliver) - return - } - - // Skip already processed store packets - if (that._packetIdsDuringStoreProcessing[packet.messageId]) { - storeDeliver() - return - } - - // Avoid unnecessary stream read operations when disconnected - if (!that.disconnecting && !that.reconnectTimer) { - cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null - that.outgoing[packet.messageId] = { - volatile: false, - cb: function (err, status) { - // Ensure that the original callback passed in to publish gets invoked - if (cb) { - cb(err, status) - } - - storeDeliver() - } - } - that._packetIdsDuringStoreProcessing[packet.messageId] = true - if (that.messageIdProvider.register(packet.messageId)) { - that._sendPacket(packet) - } else { - debug('messageId: %d has already used.', packet.messageId) - } - } else if (outStore.destroy) { - outStore.destroy() - } - } - - outStore.on('end', function () { - var allProcessed = true - for (var id in that._packetIdsDuringStoreProcessing) { - if (!that._packetIdsDuringStoreProcessing[id]) { - allProcessed = false - break - } - } - if (allProcessed) { - clearStoreProcessing() - that.removeListener('close', remove) - that._invokeAllStoreProcessingQueue() - that.emit('connect', packet) - } else { - startStreamProcess() - } - }) - storeDeliver() - } - // start flowing - startStreamProcess() -} - -MqttClient.prototype._invokeStoreProcessingQueue = function () { - if (this._storeProcessingQueue.length > 0) { - var f = this._storeProcessingQueue[0] - if (f && f.invoke()) { - this._storeProcessingQueue.shift() - return true - } - } - return false -} - -MqttClient.prototype._invokeAllStoreProcessingQueue = function () { - while (this._invokeStoreProcessingQueue()) {} -} - -MqttClient.prototype._flushStoreProcessingQueue = function () { - for (var f of this._storeProcessingQueue) { - if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) - if (f.callback) f.callback(new Error('Connection closed')) - } - this._storeProcessingQueue.splice(0) -} - -module.exports = MqttClient +'use strict' + +/** + * Module dependencies + */ +var EventEmitter = require('events').EventEmitter +var Store = require('./store') +var TopicAliasRecv = require('./topic-alias-recv') +var TopicAliasSend = require('./topic-alias-send') +var mqttPacket = require('mqtt-packet') +var DefaultMessageIdProvider = require('./default-message-id-provider') +var Writable = require('readable-stream').Writable +var inherits = require('inherits') +var reInterval = require('reinterval') +var clone = require('rfdc/default') +var validations = require('./validations') +var xtend = require('xtend') +var debug = require('debug')('mqttjs:client') +var nextTick = process ? process.nextTick : function (callback) { setTimeout(callback, 0) } +var setImmediate = global.setImmediate || function (callback) { + // works in node v0.8 + nextTick(callback) +} +var defaultConnectOptions = { + keepalive: 60, + reschedulePings: true, + protocolId: 'MQTT', + protocolVersion: 4, + reconnectPeriod: 1000, + connectTimeout: 30 * 1000, + clean: true, + resubscribe: true +} + +var socketErrors = [ + 'ECONNREFUSED', + 'EADDRINUSE', + 'ECONNRESET', + 'ENOTFOUND' +] + +// Other Socket Errors: EADDRINUSE, ECONNRESET, ENOTFOUND. + +var errors = { + 0: '', + 1: 'Unacceptable protocol version', + 2: 'Identifier rejected', + 3: 'Server unavailable', + 4: 'Bad username or password', + 5: 'Not authorized', + 16: 'No matching subscribers', + 17: 'No subscription existed', + 128: 'Unspecified error', + 129: 'Malformed Packet', + 130: 'Protocol Error', + 131: 'Implementation specific error', + 132: 'Unsupported Protocol Version', + 133: 'Client Identifier not valid', + 134: 'Bad User Name or Password', + 135: 'Not authorized', + 136: 'Server unavailable', + 137: 'Server busy', + 138: 'Banned', + 139: 'Server shutting down', + 140: 'Bad authentication method', + 141: 'Keep Alive timeout', + 142: 'Session taken over', + 143: 'Topic Filter invalid', + 144: 'Topic Name invalid', + 145: 'Packet identifier in use', + 146: 'Packet Identifier not found', + 147: 'Receive Maximum exceeded', + 148: 'Topic Alias invalid', + 149: 'Packet too large', + 150: 'Message rate too high', + 151: 'Quota exceeded', + 152: 'Administrative action', + 153: 'Payload format invalid', + 154: 'Retain not supported', + 155: 'QoS not supported', + 156: 'Use another server', + 157: 'Server moved', + 158: 'Shared Subscriptions not supported', + 159: 'Connection rate exceeded', + 160: 'Maximum connect time', + 161: 'Subscription Identifiers not supported', + 162: 'Wildcard Subscriptions not supported' +} + +function defaultId () { + return 'mqttjs_' + Math.random().toString(16).substr(2, 8) +} + +function applyTopicAlias (client, packet) { + if (client.options.protocolVersion === 5) { + if (packet.cmd === 'publish') { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + var topic = packet.topic.toString() + if (client.topicAliasSend) { + if (alias) { + if (topic.length !== 0) { + // register topic alias + debug('applyTopicAlias :: register topic: %s - alias: %d', topic, alias) + if (!client.topicAliasSend.put(topic, alias)) { + debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + return new Error('Sending Topic Alias out of range') + } + } + } else { + if (topic.length !== 0) { + if (client.options.autoAssignTopicAlias) { + alias = client.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto assign(use) topic: %s - alias: %d', topic, alias) + } else { + alias = client.topicAliasSend.getLruAlias() + client.topicAliasSend.put(topic, alias) + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto assign topic: %s - alias: %d', topic, alias) + } + } else if (client.options.autoUseTopicAlias) { + alias = client.topicAliasSend.getAliasByTopic(topic) + if (alias) { + packet.topic = '' + packet.properties = {...(packet.properties), topicAlias: alias} + debug('applyTopicAlias :: auto use topic: %s - alias: %d', topic, alias) + } + } + } + } + } else if (alias) { + debug('applyTopicAlias :: error out of range. topic: %s - alias: %d', topic, alias) + return new Error('Sending Topic Alias out of range') + } + } + } +} + +function removeTopicAliasAndRecoverTopicName (client, packet) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + + var topic = packet.topic.toString() + if (topic.length === 0) { + // restore topic from alias + if (typeof alias === 'undefined') { + return new Error('Unregistered Topic Alias') + } else { + topic = client.topicAliasSend.getTopicByAlias(alias) + if (typeof topic === 'undefined') { + return new Error('Unregistered Topic Alias') + } else { + packet.topic = topic + } + } + } + if (alias) { + delete packet.properties.topicAlias + } +} + +function sendPacket (client, packet, cb) { + debug('sendPacket :: packet: %O', packet) + debug('sendPacket :: emitting `packetsend`') + + client.emit('packetsend', packet) + + debug('sendPacket :: writing to stream') + var result = mqttPacket.writeToStream(packet, client.stream, client.options) + debug('sendPacket :: writeToStream result %s', result) + if (!result && cb) { + debug('sendPacket :: handle events on `drain` once through callback.') + client.stream.once('drain', cb) + } else if (cb) { + debug('sendPacket :: invoking cb') + cb() + } +} + +function flush (queue) { + if (queue) { + debug('flush: queue exists? %b', !!(queue)) + Object.keys(queue).forEach(function (messageId) { + if (typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } +} + +function flushVolatile (queue) { + if (queue) { + debug('flushVolatile :: deleting volatile messages from the queue and setting their callbacks as error function') + Object.keys(queue).forEach(function (messageId) { + if (queue[messageId].volatile && typeof queue[messageId].cb === 'function') { + queue[messageId].cb(new Error('Connection closed')) + delete queue[messageId] + } + }) + } +} + +function storeAndSend (client, packet, cb, cbStorePut) { + debug('storeAndSend :: store packet with cmd %s to outgoingStore', packet.cmd) + var storePacket = packet + var err + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + err = removeTopicAliasAndRecoverTopicName(client, storePacket) + if (err) { + return cb && cb(err) + } + } + client.outgoingStore.put(storePacket, function storedPacket (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + sendPacket(client, packet, cb) + }) +} + +function nop (error) { + debug('nop ::', error) +} + +/** + * MqttClient constructor + * + * @param {Stream} stream - stream + * @param {Object} [options] - connection options + * (see Connection#connect) + */ +function MqttClient (streamBuilder, options) { + var k + var that = this + + if (!(this instanceof MqttClient)) { + return new MqttClient(streamBuilder, options) + } + + this.options = options || {} + + // Defaults + for (k in defaultConnectOptions) { + if (typeof this.options[k] === 'undefined') { + this.options[k] = defaultConnectOptions[k] + } else { + this.options[k] = options[k] + } + } + + debug('MqttClient :: options.protocol', options.protocol) + debug('MqttClient :: options.protocolVersion', options.protocolVersion) + debug('MqttClient :: options.username', options.username) + debug('MqttClient :: options.keepalive', options.keepalive) + debug('MqttClient :: options.reconnectPeriod', options.reconnectPeriod) + debug('MqttClient :: options.rejectUnauthorized', options.rejectUnauthorized) + debug('MqttClient :: options.topicAliasMaximum', options.topicAliasMaximum) + + this.options.clientId = (typeof options.clientId === 'string') ? options.clientId : defaultId() + + debug('MqttClient :: clientId', this.options.clientId) + + this.options.customHandleAcks = (options.protocolVersion === 5 && options.customHandleAcks) ? options.customHandleAcks : function () { arguments[3](0) } + + this.streamBuilder = streamBuilder + + this.messageIdProvider = (typeof this.options.messageIdProvider === 'undefined') ? new DefaultMessageIdProvider() : this.options.messageIdProvider + + // Inflight message storages + this.outgoingStore = options.outgoingStore || new Store() + this.incomingStore = options.incomingStore || new Store() + + // Should QoS zero messages be queued when the connection is broken? + this.queueQoSZero = options.queueQoSZero === undefined ? true : options.queueQoSZero + + // map of subscribed topics to support reconnection + this._resubscribeTopics = {} + + // map of a subscribe messageId and a topic + this.messageIdToTopic = {} + + // Ping timer, setup in _setupPingTimer + this.pingTimer = null + // Is the client connected? + this.connected = false + // Are we disconnecting? + this.disconnecting = false + // Packet queue + this.queue = [] + // connack timer + this.connackTimer = null + // Reconnect timer + this.reconnectTimer = null + // Is processing store? + this._storeProcessing = false + // Packet Ids are put into the store during store processing + this._packetIdsDuringStoreProcessing = {} + // Store processing queue + this._storeProcessingQueue = [] + + // Inflight callbacks + this.outgoing = {} + + // True if connection is first time. + this._firstConnection = true + + if (options.topicAliasMaximum > 0) { + if (options.topicAliasMaximum > 0xffff) { + debug('MqttClient :: options.topicAliasMaximum is out of range') + } else { + this.topicAliasRecv = new TopicAliasRecv(options.topicAliasMaximum) + } + } + + // Send queued packets + this.on('connect', function () { + var queue = this.queue + + function deliver () { + var entry = queue.shift() + debug('deliver :: entry %o', entry) + var packet = null + + if (!entry) { + that._resubscribe() + return + } + + packet = entry.packet + debug('deliver :: call _sendPacket for %o', packet) + var send = true + if (packet.messageId && packet.messageId !== 0) { + if (!that.messageIdProvider.register(packet.messageId)) { + send = false + } + } + if (send) { + that._sendPacket( + packet, + function (err) { + if (entry.cb) { + entry.cb(err) + } + deliver() + } + ) + } else { + debug('messageId: %d has already used. The message is skipped and removed.', packet.messageId) + deliver() + } + } + + debug('connect :: sending queued packets') + deliver() + }) + + this.on('close', function () { + debug('close :: connected set to `false`') + this.connected = false + + debug('close :: clearing connackTimer') + clearTimeout(this.connackTimer) + + debug('close :: clearing ping timer') + if (that.pingTimer !== null) { + that.pingTimer.clear() + that.pingTimer = null + } + + if (this.topicAliasRecv) { + this.topicAliasRecv.clear() + } + + debug('close :: calling _setupReconnect') + this._setupReconnect() + }) + EventEmitter.call(this) + + debug('MqttClient :: setting up stream') + this._setupStream() +} +inherits(MqttClient, EventEmitter) + +/** + * setup the event handlers in the inner stream. + * + * @api private + */ +MqttClient.prototype._setupStream = function () { + var connectPacket + var that = this + var writable = new Writable() + var parser = mqttPacket.parser(this.options) + var completeParse = null + var packets = [] + + debug('_setupStream :: calling method to clear reconnect') + this._clearReconnect() + + debug('_setupStream :: using streamBuilder provided to client to create stream') + this.stream = this.streamBuilder(this) + + parser.on('packet', function (packet) { + debug('parser :: on packet push to packets array.') + packets.push(packet) + }) + + function nextTickWork () { + if (packets.length) { + nextTick(work) + } else { + var done = completeParse + completeParse = null + done() + } + } + + function work () { + debug('work :: getting next packet in queue') + var packet = packets.shift() + + if (packet) { + debug('work :: packet pulled from queue') + that._handlePacket(packet, nextTickWork) + } else { + debug('work :: no packets in queue') + var done = completeParse + completeParse = null + debug('work :: done flag is %s', !!(done)) + if (done) done() + } + } + + writable._write = function (buf, enc, done) { + completeParse = done + debug('writable stream :: parsing buffer') + parser.parse(buf) + work() + } + + function streamErrorHandler (error) { + debug('streamErrorHandler :: error', error.message) + if (socketErrors.includes(error.code)) { + // handle error + debug('streamErrorHandler :: emitting error') + that.emit('error', error) + } else { + nop(error) + } + } + + debug('_setupStream :: pipe stream to writable stream') + this.stream.pipe(writable) + + // Suppress connection errors + this.stream.on('error', streamErrorHandler) + + // Echo stream close + this.stream.on('close', function () { + debug('(%s)stream :: on close', that.options.clientId) + flushVolatile(that.outgoing) + debug('stream: emit close to MqttClient') + that.emit('close') + }) + + // Send a connect packet + debug('_setupStream: sending packet `connect`') + connectPacket = Object.create(this.options) + connectPacket.cmd = 'connect' + if (this.topicAliasRecv) { + if (!connectPacket.properties) { + connectPacket.properties = {} + } + if (this.topicAliasRecv) { + connectPacket.properties.topicAliasMaximum = this.topicAliasRecv.max + } + } + // avoid message queue + sendPacket(this, connectPacket) + + // Echo connection errors + parser.on('error', this.emit.bind(this, 'error')) + + // auth + if (this.options.properties) { + if (!this.options.properties.authenticationMethod && this.options.properties.authenticationData) { + that.end(() => + this.emit('error', new Error('Packet has no Authentication Method') + )) + return this + } + if (this.options.properties.authenticationMethod && this.options.authPacket && typeof this.options.authPacket === 'object') { + var authPacket = xtend({cmd: 'auth', reasonCode: 0}, this.options.authPacket) + sendPacket(this, authPacket) + } + } + + // many drain listeners are needed for qos 1 callbacks if the connection is intermittent + this.stream.setMaxListeners(1000) + + clearTimeout(this.connackTimer) + this.connackTimer = setTimeout(function () { + debug('!!connectTimeout hit!! Calling _cleanUp with force `true`') + that._cleanUp(true) + }, this.options.connectTimeout) +} + +MqttClient.prototype._handlePacket = function (packet, done) { + var options = this.options + + if (options.protocolVersion === 5 && options.properties && options.properties.maximumPacketSize && options.properties.maximumPacketSize < packet.length) { + this.emit('error', new Error('exceeding packets size ' + packet.cmd)) + this.end({reasonCode: 149, properties: { reasonString: 'Maximum packet size was exceeded' }}) + return this + } + debug('_handlePacket :: emitting packetreceive') + this.emit('packetreceive', packet) + + switch (packet.cmd) { + case 'publish': + this._handlePublish(packet, done) + break + case 'puback': + case 'pubrec': + case 'pubcomp': + case 'suback': + case 'unsuback': + this._handleAck(packet) + done() + break + case 'pubrel': + this._handlePubrel(packet, done) + break + case 'connack': + this._handleConnack(packet) + done() + break + case 'pingresp': + this._handlePingresp(packet) + done() + break + case 'disconnect': + this._handleDisconnect(packet) + done() + break + default: + // do nothing + // maybe we should do an error handling + // or just log it + break + } +} + +MqttClient.prototype._checkDisconnecting = function (callback) { + if (this.disconnecting) { + if (callback) { + callback(new Error('client disconnecting')) + } else { + this.emit('error', new Error('client disconnecting')) + } + } + return this.disconnecting +} + +/** + * publish - publish to + * + * @param {String} topic - topic to publish to + * @param {String, Buffer} message - message to publish + * @param {Object} [opts] - publish options, includes: + * {Number} qos - qos level to publish on + * {Boolean} retain - whether or not to retain the message + * {Boolean} dup - whether or not mark a message as duplicate + * {Function} cbStorePut - function(){} called when message is put into `outgoingStore` + * @param {Function} [callback] - function(err){} + * called when publish succeeds or fails + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.publish('topic', 'message'); + * @example + * client.publish('topic', 'message', {qos: 1, retain: true, dup: true}); + * @example client.publish('topic', 'message', console.log); + */ +MqttClient.prototype.publish = function (topic, message, opts, callback) { + debug('publish :: message `%s` to topic `%s`', message, topic) + var packet + var options = this.options + + // .publish(topic, payload, cb); + if (typeof opts === 'function') { + callback = opts + opts = null + } + + // default opts + var defaultOpts = {qos: 0, retain: false, dup: false} + opts = xtend(defaultOpts, opts) + + if (this._checkDisconnecting(callback)) { + return this + } + + var that = this + var publishProc = function () { + var messageId = 0 + if (opts.qos === 1 || opts.qos === 2) { + messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + } + packet = { + cmd: 'publish', + topic: topic, + payload: message, + qos: opts.qos, + retain: opts.retain, + messageId: messageId, + dup: opts.dup + } + + if (options.protocolVersion === 5) { + packet.properties = opts.properties + } + + debug('publish :: qos', opts.qos) + switch (opts.qos) { + case 1: + case 2: + // Add to callbacks + that.outgoing[packet.messageId] = { + volatile: false, + cb: callback || nop + } + debug('MqttClient:publish: packet cmd: %s', packet.cmd) + that._sendPacket(packet, undefined, opts.cbStorePut) + break + default: + debug('MqttClient:publish: packet cmd: %s', packet.cmd) + that._sendPacket(packet, callback, opts.cbStorePut) + break + } + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': publishProc, + 'cbStorePut': opts.cbStorePut, + 'callback': callback + } + ) + } else { + publishProc() + } + return this +} + +/** + * subscribe - subscribe to + * + * @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos} + * @param {Object} [opts] - optional subscription options, includes: + * {Number} qos - subscribe qos level + * @param {Function} [callback] - function(err, granted){} where: + * {Error} err - subscription error (none at the moment!) + * {Array} granted - array of {topic: 't', qos: 0} + * @returns {MqttClient} this - for chaining + * @api public + * @example client.subscribe('topic'); + * @example client.subscribe('topic', {qos: 1}); + * @example client.subscribe({'topic': {qos: 0}, 'topic2': {qos: 1}}, console.log); + * @example client.subscribe('topic', console.log); + */ +MqttClient.prototype.subscribe = function () { + var that = this + var args = new Array(arguments.length) + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i] + } + var subs = [] + var obj = args.shift() + var resubscribe = obj.resubscribe + var callback = args.pop() || nop + var opts = args.pop() + var version = this.options.protocolVersion + + delete obj.resubscribe + + if (typeof obj === 'string') { + obj = [obj] + } + + if (typeof callback !== 'function') { + opts = callback + callback = nop + } + + var invalidTopic = validations.validateTopics(obj) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) + return this + } + + if (this._checkDisconnecting(callback)) { + debug('subscribe: discconecting true') + return this + } + + var defaultOpts = { + qos: 0 + } + if (version === 5) { + defaultOpts.nl = false + defaultOpts.rap = false + defaultOpts.rh = 0 + } + opts = xtend(defaultOpts, opts) + + if (Array.isArray(obj)) { + obj.forEach(function (topic) { + debug('subscribe: array topic %s', topic) + if (!that._resubscribeTopics.hasOwnProperty(topic) || + that._resubscribeTopics[topic].qos < opts.qos || + resubscribe) { + var currentOpts = { + topic: topic, + qos: opts.qos + } + if (version === 5) { + currentOpts.nl = opts.nl + currentOpts.rap = opts.rap + currentOpts.rh = opts.rh + currentOpts.properties = opts.properties + } + debug('subscribe: pushing topic `%s` and qos `%s` to subs list', currentOpts.topic, currentOpts.qos) + subs.push(currentOpts) + } + }) + } else { + Object + .keys(obj) + .forEach(function (k) { + debug('subscribe: object topic %s', k) + if (!that._resubscribeTopics.hasOwnProperty(k) || + that._resubscribeTopics[k].qos < obj[k].qos || + resubscribe) { + var currentOpts = { + topic: k, + qos: obj[k].qos + } + if (version === 5) { + currentOpts.nl = obj[k].nl + currentOpts.rap = obj[k].rap + currentOpts.rh = obj[k].rh + currentOpts.properties = opts.properties + } + debug('subscribe: pushing `%s` to subs list', currentOpts) + subs.push(currentOpts) + } + }) + } + + if (!subs.length) { + callback(null, []) + return this + } + + var subscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + + var packet = { + cmd: 'subscribe', + subscriptions: subs, + qos: 1, + retain: false, + dup: false, + messageId: messageId + } + + if (opts.properties) { + packet.properties = opts.properties + } + + // subscriptions to resubscribe to in case of disconnect + if (that.options.resubscribe) { + debug('subscribe :: resubscribe true') + var topics = [] + subs.forEach(function (sub) { + if (that.options.reconnectPeriod > 0) { + var topic = { qos: sub.qos } + if (version === 5) { + topic.nl = sub.nl || false + topic.rap = sub.rap || false + topic.rh = sub.rh || 0 + topic.properties = sub.properties + } + that._resubscribeTopics[sub.topic] = topic + topics.push(sub.topic) + } + }) + that.messageIdToTopic[packet.messageId] = topics + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: function (err, packet) { + if (!err) { + var granted = packet.granted + for (var i = 0; i < granted.length; i += 1) { + subs[i].qos = granted[i] + } + } + + callback(err, subs) + } + } + debug('subscribe :: call _sendPacket') + that._sendPacket(packet) + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': subscribeProc, + 'callback': callback + } + ) + } else { + subscribeProc() + } + + return this +} + +/** + * unsubscribe - unsubscribe from topic(s) + * + * @param {String, Array} topic - topics to unsubscribe from + * @param {Object} [opts] - optional subscription options, includes: + * {Object} properties - properties of unsubscribe packet + * @param {Function} [callback] - callback fired on unsuback + * @returns {MqttClient} this - for chaining + * @api public + * @example client.unsubscribe('topic'); + * @example client.unsubscribe('topic', console.log); + */ +MqttClient.prototype.unsubscribe = function () { + var that = this + var args = new Array(arguments.length) + for (var i = 0; i < arguments.length; i++) { + args[i] = arguments[i] + } + var topic = args.shift() + var callback = args.pop() || nop + var opts = args.pop() + if (typeof topic === 'string') { + topic = [topic] + } + + if (typeof callback !== 'function') { + opts = callback + callback = nop + } + + var invalidTopic = validations.validateTopics(topic) + if (invalidTopic !== null) { + setImmediate(callback, new Error('Invalid topic ' + invalidTopic)) + return this + } + + if (that._checkDisconnecting(callback)) { + return this + } + + var unsubscribeProc = function () { + var messageId = that._nextId() + if (messageId === null) { + debug('No messageId left') + return false + } + var packet = { + cmd: 'unsubscribe', + qos: 1, + messageId: messageId + } + + if (typeof topic === 'string') { + packet.unsubscriptions = [topic] + } else if (Array.isArray(topic)) { + packet.unsubscriptions = topic + } + + if (that.options.resubscribe) { + packet.unsubscriptions.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } + + if (typeof opts === 'object' && opts.properties) { + packet.properties = opts.properties + } + + that.outgoing[packet.messageId] = { + volatile: true, + cb: callback + } + + debug('unsubscribe: call _sendPacket') + that._sendPacket(packet) + + return true + } + + if (this._storeProcessing || this._storeProcessingQueue.length > 0) { + this._storeProcessingQueue.push( + { + 'invoke': unsubscribeProc, + 'callback': callback + } + ) + } else { + unsubscribeProc() + } + + return this +} + +/** + * end - close connection + * + * @returns {MqttClient} this - for chaining + * @param {Boolean} force - do not wait for all in-flight messages to be acked + * @param {Object} opts - added to the disconnect packet + * @param {Function} cb - called when the client has been closed + * + * @api public + */ +MqttClient.prototype.end = function (force, opts, cb) { + var that = this + + debug('end :: (%s)', this.options.clientId) + + if (force == null || typeof force !== 'boolean') { + cb = opts || nop + opts = force + force = false + if (typeof opts !== 'object') { + cb = opts + opts = null + if (typeof cb !== 'function') { + cb = nop + } + } + } + + if (typeof opts !== 'object') { + cb = opts + opts = null + } + + debug('end :: cb? %s', !!cb) + cb = cb || nop + + function closeStores () { + debug('end :: closeStores: closing incoming and outgoing stores') + that.disconnected = true + that.incomingStore.close(function (e1) { + that.outgoingStore.close(function (e2) { + debug('end :: closeStores: emitting end') + that.emit('end') + if (cb) { + let err = e1 || e2 + debug('end :: closeStores: invoking callback with args') + cb(err) + } + }) + }) + if (that._deferredReconnect) { + that._deferredReconnect() + } + } + + function finish () { + // defer closesStores of an I/O cycle, + // just to make sure things are + // ok for websockets + debug('end :: (%s) :: finish :: calling _cleanUp with force %s', that.options.clientId, force) + that._cleanUp(force, () => { + debug('end :: finish :: calling process.nextTick on closeStores') + // var boundProcess = nextTick.bind(null, closeStores) + nextTick(closeStores.bind(that)) + }, opts) + } + + if (this.disconnecting) { + cb() + return this + } + + this._clearReconnect() + + this.disconnecting = true + + if (!force && Object.keys(this.outgoing).length > 0) { + // wait 10ms, just to be sure we received all of it + debug('end :: (%s) :: calling finish in 10ms once outgoing is empty', that.options.clientId) + this.once('outgoingEmpty', setTimeout.bind(null, finish, 10)) + } else { + debug('end :: (%s) :: immediately calling finish', that.options.clientId) + finish() + } + + return this +} + +/** + * removeOutgoingMessage - remove a message in outgoing store + * the outgoing callback will be called withe Error('Message removed') if the message is removed + * + * @param {Number} messageId - messageId to remove message + * @returns {MqttClient} this - for chaining + * @api public + * + * @example client.removeOutgoingMessage(client.getLastAllocated()); + */ +MqttClient.prototype.removeOutgoingMessage = function (messageId) { + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + delete this.outgoing[messageId] + this.outgoingStore.del({messageId: messageId}, function () { + cb(new Error('Message removed')) + }) + return this +} + +/** + * reconnect - connect again using the same options as connect() + * + * @param {Object} [opts] - optional reconnect options, includes: + * {Store} incomingStore - a store for the incoming packets + * {Store} outgoingStore - a store for the outgoing packets + * if opts is not given, current stores are used + * @returns {MqttClient} this - for chaining + * + * @api public + */ +MqttClient.prototype.reconnect = function (opts) { + debug('client reconnect') + var that = this + var f = function () { + if (opts) { + that.options.incomingStore = opts.incomingStore + that.options.outgoingStore = opts.outgoingStore + } else { + that.options.incomingStore = null + that.options.outgoingStore = null + } + that.incomingStore = that.options.incomingStore || new Store() + that.outgoingStore = that.options.outgoingStore || new Store() + that.disconnecting = false + that.disconnected = false + that._deferredReconnect = null + that._reconnect() + } + + if (this.disconnecting && !this.disconnected) { + this._deferredReconnect = f + } else { + f() + } + return this +} + +/** + * _reconnect - implement reconnection + * @api privateish + */ +MqttClient.prototype._reconnect = function () { + debug('_reconnect: emitting reconnect to client') + this.emit('reconnect') + if (this.connected) { + this.end(() => { this._setupStream() }) + debug('client already connected. disconnecting first.') + } else { + debug('_reconnect: calling _setupStream') + this._setupStream() + } +} + +/** + * _setupReconnect - setup reconnect timer + */ +MqttClient.prototype._setupReconnect = function () { + var that = this + + if (!that.disconnecting && !that.reconnectTimer && (that.options.reconnectPeriod > 0)) { + if (!this.reconnecting) { + debug('_setupReconnect :: emit `offline` state') + this.emit('offline') + debug('_setupReconnect :: set `reconnecting` to `true`') + this.reconnecting = true + } + debug('_setupReconnect :: setting reconnectTimer for %d ms', that.options.reconnectPeriod) + that.reconnectTimer = setInterval(function () { + debug('reconnectTimer :: reconnect triggered!') + that._reconnect() + }, that.options.reconnectPeriod) + } else { + debug('_setupReconnect :: doing nothing...') + } +} + +/** + * _clearReconnect - clear the reconnect timer + */ +MqttClient.prototype._clearReconnect = function () { + debug('_clearReconnect : clearing reconnect timer') + if (this.reconnectTimer) { + clearInterval(this.reconnectTimer) + this.reconnectTimer = null + } +} + +/** + * _cleanUp - clean up on connection end + * @api private + */ +MqttClient.prototype._cleanUp = function (forced, done) { + var opts = arguments[2] + if (done) { + debug('_cleanUp :: done callback provided for on stream close') + this.stream.on('close', done) + } + + debug('_cleanUp :: forced? %s', forced) + if (forced) { + if ((this.options.reconnectPeriod === 0) && this.options.clean) { + flush(this.outgoing) + } + debug('_cleanUp :: (%s) :: destroying stream', this.options.clientId) + this.stream.destroy() + } else { + var packet = xtend({ cmd: 'disconnect' }, opts) + debug('_cleanUp :: (%s) :: call _sendPacket with disconnect packet', this.options.clientId) + this._sendPacket( + packet, + setImmediate.bind( + null, + this.stream.end.bind(this.stream) + ) + ) + } + + if (!this.disconnecting) { + debug('_cleanUp :: client not disconnecting. Clearing and resetting reconnect.') + this._clearReconnect() + this._setupReconnect() + } + + if (this.pingTimer !== null) { + debug('_cleanUp :: clearing pingTimer') + this.pingTimer.clear() + this.pingTimer = null + } + + if (done && !this.connected) { + debug('_cleanUp :: (%s) :: removing stream `done` callback `close` listener', this.options.clientId) + this.stream.removeListener('close', done) + done() + } +} + +/** + * _sendPacket - send or queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ +MqttClient.prototype._sendPacket = function (packet, cb, cbStorePut) { + debug('_sendPacket :: (%s) :: start', this.options.clientId) + cbStorePut = cbStorePut || nop + cb = cb || nop + + var err = applyTopicAlias(this, packet) + if (err) { + cb(err) + return + } + + if (!this.connected) { + debug('_sendPacket :: client not connected. Storing packet offline.') + this._storePacket(packet, cb, cbStorePut) + return + } + + // When sending a packet, reschedule the ping timer + this._shiftPingInterval() + + switch (packet.cmd) { + case 'publish': + break + case 'pubrel': + storeAndSend(this, packet, cb, cbStorePut) + return + default: + sendPacket(this, packet, cb) + return + } + + switch (packet.qos) { + case 2: + case 1: + storeAndSend(this, packet, cb, cbStorePut) + break + /** + * no need of case here since it will be caught by default + * and jshint comply that before default it must be a break + * anyway it will result in -1 evaluation + */ + case 0: + /* falls through */ + default: + sendPacket(this, packet, cb) + break + } + debug('_sendPacket :: (%s) :: end', this.options.clientId) +} + +/** + * _storePacket - queue a packet + * @param {Object} packet - packet options + * @param {Function} cb - callback when the packet is sent + * @param {Function} cbStorePut - called when message is put into outgoingStore + * @api private + */ +MqttClient.prototype._storePacket = function (packet, cb, cbStorePut) { + debug('_storePacket :: packet: %o', packet) + debug('_storePacket :: cb? %s', !!cb) + cbStorePut = cbStorePut || nop + + var storePacket = packet + if (storePacket.cmd === 'publish') { + // The original packet is for sending. + // The cloned storePacket is for storing to resend on reconnect. + // Topic Alias must not be used after disconnected. + storePacket = clone(packet) + var err = removeTopicAliasAndRecoverTopicName(this, storePacket) + if (err) { + return cb && cb(err) + } + } + // check that the packet is not a qos of 0, or that the command is not a publish + if (((storePacket.qos || 0) === 0 && this.queueQoSZero) || storePacket.cmd !== 'publish') { + this.queue.push({ packet: storePacket, cb: cb }) + } else if (storePacket.qos > 0) { + cb = this.outgoing[storePacket.messageId] ? this.outgoing[storePacket.messageId].cb : null + this.outgoingStore.put(storePacket, function (err) { + if (err) { + return cb && cb(err) + } + cbStorePut() + }) + } else if (cb) { + cb(new Error('No connection to broker')) + } +} + +/** + * _setupPingTimer - setup the ping timer + * + * @api private + */ +MqttClient.prototype._setupPingTimer = function () { + debug('_setupPingTimer :: keepalive %d (seconds)', this.options.keepalive) + var that = this + + if (!this.pingTimer && this.options.keepalive) { + this.pingResp = true + this.pingTimer = reInterval(function () { + that._checkPing() + }, this.options.keepalive * 1000) + } +} + +/** + * _shiftPingInterval - reschedule the ping interval + * + * @api private + */ +MqttClient.prototype._shiftPingInterval = function () { + if (this.pingTimer && this.options.keepalive && this.options.reschedulePings) { + this.pingTimer.reschedule(this.options.keepalive * 1000) + } +} +/** + * _checkPing - check if a pingresp has come back, and ping the server again + * + * @api private + */ +MqttClient.prototype._checkPing = function () { + debug('_checkPing :: checking ping...') + if (this.pingResp) { + debug('_checkPing :: ping response received. Clearing flag and sending `pingreq`') + this.pingResp = false + this._sendPacket({ cmd: 'pingreq' }) + } else { + // do a forced cleanup since socket will be in bad shape + debug('_checkPing :: calling _cleanUp with force true') + this._cleanUp(true) + } +} + +/** + * _handlePingresp - handle a pingresp + * + * @api private + */ +MqttClient.prototype._handlePingresp = function () { + this.pingResp = true +} + +/** + * _handleConnack + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handleConnack = function (packet) { + debug('_handleConnack') + var options = this.options + var version = options.protocolVersion + var rc = version === 5 ? packet.reasonCode : packet.returnCode + + clearTimeout(this.connackTimer) + delete this.topicAliasSend + + if (packet.properties) { + if (packet.properties.topicAliasMaximum) { + if (packet.properties.topicAliasMaximum > 0xffff) { + this.emit('error', new Error('topicAliasMaximum from broker is out of range')) + return + } + if (packet.properties.topicAliasMaximum > 0) { + this.topicAliasSend = new TopicAliasSend(packet.properties.topicAliasMaximum) + } + } + if (packet.properties.serverKeepAlive && options.keepalive) { + options.keepalive = packet.properties.serverKeepAlive + this._shiftPingInterval() + } + if (packet.properties.maximumPacketSize) { + if (!options.properties) { options.properties = {} } + options.properties.maximumPacketSize = packet.properties.maximumPacketSize + } + } + + if (rc === 0) { + this.reconnecting = false + this._onConnect(packet) + } else if (rc > 0) { + var err = new Error('Connection refused: ' + errors[rc]) + err.code = rc + this.emit('error', err) + } +} + +/** + * _handlePublish + * + * @param {Object} packet + * @api private + */ +/* +those late 2 case should be rewrite to comply with coding style: + +case 1: +case 0: + // do not wait sending a puback + // no callback passed + if (1 === qos) { + this._sendPacket({ + cmd: 'puback', + messageId: messageId + }); + } + // emit the message event for both qos 1 and 0 + this.emit('message', topic, message, packet); + this.handleMessage(packet, done); + break; +default: + // do nothing but every switch mus have a default + // log or throw an error about unknown qos + break; + +for now i just suppressed the warnings +*/ +MqttClient.prototype._handlePublish = function (packet, done) { + debug('_handlePublish: packet %o', packet) + done = typeof done !== 'undefined' ? done : nop + var topic = packet.topic.toString() + var message = packet.payload + var qos = packet.qos + var messageId = packet.messageId + var that = this + var options = this.options + var validReasonCodes = [0, 16, 128, 131, 135, 144, 145, 151, 153] + if (this.options.protocolVersion === 5) { + var alias + if (packet.properties) { + alias = packet.properties.topicAlias + } + if (typeof alias !== 'undefined') { + if (topic.length === 0) { + if (alias > 0 && alias <= 0xffff) { + var gotTopic = this.topicAliasRecv.getTopicByAlias(alias) + if (gotTopic) { + topic = gotTopic + debug('_handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: unregistered topic alias. alias: %d', alias) + this.emit('error', new Error('Received unregistered Topic Alias')) + return + } + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } else { + if (this.topicAliasRecv.put(topic, alias)) { + debug('_handlePublish :: registered topic: %s - alias: %d', topic, alias) + } else { + debug('_handlePublish :: topic alias out of range. alias: %d', alias) + this.emit('error', new Error('Received Topic Alias is out of range')) + return + } + } + } + } + debug('_handlePublish: qos %d', qos) + switch (qos) { + case 2: { + options.customHandleAcks(topic, message, packet, function (error, code) { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { return that.emit('error', error) } + if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for pubrec')) } + if (code) { + that._sendPacket({cmd: 'pubrec', messageId: messageId, reasonCode: code}, done) + } else { + that.incomingStore.put(packet, function () { + that._sendPacket({cmd: 'pubrec', messageId: messageId}, done) + }) + } + }) + break + } + case 1: { + // emit the message event + options.customHandleAcks(topic, message, packet, function (error, code) { + if (!(error instanceof Error)) { + code = error + error = null + } + if (error) { return that.emit('error', error) } + if (validReasonCodes.indexOf(code) === -1) { return that.emit('error', new Error('Wrong reason code for puback')) } + if (!code) { that.emit('message', topic, message, packet) } + that.handleMessage(packet, function (err) { + if (err) { + return done && done(err) + } + that._sendPacket({cmd: 'puback', messageId: messageId, reasonCode: code}, done) + }) + }) + break + } + case 0: + // emit the message event + this.emit('message', topic, message, packet) + this.handleMessage(packet, done) + break + default: + // do nothing + debug('_handlePublish: unknown QoS. Doing nothing.') + // log or throw an error about unknown qos + break + } +} + +/** + * Handle messages with backpressure support, one at a time. + * Override at will. + * + * @param Packet packet the packet + * @param Function callback call when finished + * @api public + */ +MqttClient.prototype.handleMessage = function (packet, callback) { + callback() +} + +/** + * _handleAck + * + * @param {Object} packet + * @api private + */ + +MqttClient.prototype._handleAck = function (packet) { + /* eslint no-fallthrough: "off" */ + var messageId = packet.messageId + var type = packet.cmd + var response = null + var cb = this.outgoing[messageId] ? this.outgoing[messageId].cb : null + var that = this + var err + + if (!cb) { + debug('_handleAck :: Server sent an ack in error. Ignoring.') + // Server sent an ack in error, ignore it. + return + } + + // Process + debug('_handleAck :: packet type', type) + switch (type) { + case 'pubcomp': + // same thing as puback for QoS 2 + case 'puback': + var pubackRC = packet.reasonCode + // Callback - we're done + if (pubackRC && pubackRC > 0 && pubackRC !== 16) { + err = new Error('Publish error: ' + errors[pubackRC]) + err.code = pubackRC + cb(err, packet) + } + delete this.outgoing[messageId] + this.outgoingStore.del(packet, cb) + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() + break + case 'pubrec': + response = { + cmd: 'pubrel', + qos: 2, + messageId: messageId + } + var pubrecRC = packet.reasonCode + + if (pubrecRC && pubrecRC > 0 && pubrecRC !== 16) { + err = new Error('Publish error: ' + errors[pubrecRC]) + err.code = pubrecRC + cb(err, packet) + } else { + this._sendPacket(response) + } + break + case 'suback': + delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + for (var grantedI = 0; grantedI < packet.granted.length; grantedI++) { + if ((packet.granted[grantedI] & 0x80) !== 0) { + // suback with Failure status + var topics = this.messageIdToTopic[messageId] + if (topics) { + topics.forEach(function (topic) { + delete that._resubscribeTopics[topic] + }) + } + } + } + this._invokeStoreProcessingQueue() + cb(null, packet) + break + case 'unsuback': + delete this.outgoing[messageId] + this.messageIdProvider.deallocate(messageId) + this._invokeStoreProcessingQueue() + cb(null) + break + default: + that.emit('error', new Error('unrecognized packet type')) + } + + if (this.disconnecting && + Object.keys(this.outgoing).length === 0) { + this.emit('outgoingEmpty') + } +} + +/** + * _handlePubrel + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handlePubrel = function (packet, callback) { + debug('handling pubrel packet') + callback = typeof callback !== 'undefined' ? callback : nop + var messageId = packet.messageId + var that = this + + var comp = {cmd: 'pubcomp', messageId: messageId} + + that.incomingStore.get(packet, function (err, pub) { + if (!err) { + that.emit('message', pub.topic, pub.payload, pub) + that.handleMessage(pub, function (err) { + if (err) { + return callback(err) + } + that.incomingStore.del(pub, nop) + that._sendPacket(comp, callback) + }) + } else { + that._sendPacket(comp, callback) + } + }) +} + +/** + * _handleDisconnect + * + * @param {Object} packet + * @api private + */ +MqttClient.prototype._handleDisconnect = function (packet) { + this.emit('disconnect', packet) +} + +/** + * _nextId + * @return unsigned int + */ +MqttClient.prototype._nextId = function () { + return this.messageIdProvider.allocate() +} + +/** + * getLastMessageId + * @return unsigned int + */ +MqttClient.prototype.getLastMessageId = function () { + return this.messageIdProvider.getLastAllocated() +} + +/** + * _resubscribe + * @api private + */ +MqttClient.prototype._resubscribe = function () { + debug('_resubscribe') + var _resubscribeTopicsKeys = Object.keys(this._resubscribeTopics) + if (!this._firstConnection && + (this.options.clean || (this.options.protocolVersion === 5 && !this.connackPacket.sessionPresent)) && + _resubscribeTopicsKeys.length > 0) { + if (this.options.resubscribe) { + if (this.options.protocolVersion === 5) { + debug('_resubscribe: protocolVersion 5') + for (var topicI = 0; topicI < _resubscribeTopicsKeys.length; topicI++) { + var resubscribeTopic = {} + resubscribeTopic[_resubscribeTopicsKeys[topicI]] = this._resubscribeTopics[_resubscribeTopicsKeys[topicI]] + resubscribeTopic.resubscribe = true + this.subscribe(resubscribeTopic, {properties: resubscribeTopic[_resubscribeTopicsKeys[topicI]].properties}) + } + } else { + this._resubscribeTopics.resubscribe = true + this.subscribe(this._resubscribeTopics) + } + } else { + this._resubscribeTopics = {} + } + } + + this._firstConnection = false +} + +/** + * _onConnect + * + * @api private + */ +MqttClient.prototype._onConnect = function (packet) { + if (this.disconnected) { + this.emit('connect', packet) + return + } + + var that = this + + this.connackPacket = packet + this.messageIdProvider.clear() + this._setupPingTimer() + + this.connected = true + + function startStreamProcess () { + var outStore = that.outgoingStore.createStream() + + function clearStoreProcessing () { + that._storeProcessing = false + that._packetIdsDuringStoreProcessing = {} + } + + that.once('close', remove) + outStore.on('error', function (err) { + clearStoreProcessing() + that._flushStoreProcessingQueue() + that.removeListener('close', remove) + that.emit('error', err) + }) + + function remove () { + outStore.destroy() + outStore = null + that._flushStoreProcessingQueue() + clearStoreProcessing() + } + + function storeDeliver () { + // edge case, we wrapped this twice + if (!outStore) { + return + } + that._storeProcessing = true + + var packet = outStore.read(1) + + var cb + + if (!packet) { + // read when data is available in the future + outStore.once('readable', storeDeliver) + return + } + + // Skip already processed store packets + if (that._packetIdsDuringStoreProcessing[packet.messageId]) { + storeDeliver() + return + } + + // Avoid unnecessary stream read operations when disconnected + if (!that.disconnecting && !that.reconnectTimer) { + cb = that.outgoing[packet.messageId] ? that.outgoing[packet.messageId].cb : null + that.outgoing[packet.messageId] = { + volatile: false, + cb: function (err, status) { + // Ensure that the original callback passed in to publish gets invoked + if (cb) { + cb(err, status) + } + + storeDeliver() + } + } + that._packetIdsDuringStoreProcessing[packet.messageId] = true + if (that.messageIdProvider.register(packet.messageId)) { + that._sendPacket(packet) + } else { + debug('messageId: %d has already used.', packet.messageId) + } + } else if (outStore.destroy) { + outStore.destroy() + } + } + + outStore.on('end', function () { + var allProcessed = true + for (var id in that._packetIdsDuringStoreProcessing) { + if (!that._packetIdsDuringStoreProcessing[id]) { + allProcessed = false + break + } + } + if (allProcessed) { + clearStoreProcessing() + that.removeListener('close', remove) + that._invokeAllStoreProcessingQueue() + that.emit('connect', packet) + } else { + startStreamProcess() + } + }) + storeDeliver() + } + // start flowing + startStreamProcess() +} + +MqttClient.prototype._invokeStoreProcessingQueue = function () { + if (this._storeProcessingQueue.length > 0) { + var f = this._storeProcessingQueue[0] + if (f && f.invoke()) { + this._storeProcessingQueue.shift() + return true + } + } + return false +} + +MqttClient.prototype._invokeAllStoreProcessingQueue = function () { + while (this._invokeStoreProcessingQueue()) {} +} + +MqttClient.prototype._flushStoreProcessingQueue = function () { + for (var f of this._storeProcessingQueue) { + if (f.cbStorePut) f.cbStorePut(new Error('Connection closed')) + if (f.callback) f.callback(new Error('Connection closed')) + } + this._storeProcessingQueue.splice(0) +} + +module.exports = MqttClient diff --git a/lib/connect/ali.js b/lib/connect/ali.js index 1cbb726a5..e7fe6a3c5 100644 --- a/lib/connect/ali.js +++ b/lib/connect/ali.js @@ -1,128 +1,128 @@ -'use strict' - -var Transform = require('readable-stream').Transform -var duplexify = require('duplexify') - -/* global FileReader */ -var my -var proxy -var stream -var isInitialized = false - -function buildProxy () { - var proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - my.sendSocketMessage({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function () { - next(new Error()) - } - }) - } - proxy._flush = function socketEnd (done) { - my.closeSocket({ - success: function () { - done() - } - }) - } - - return proxy -} - -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } -} - -function buildUrl (opts, client) { - var protocol = opts.protocol === 'alis' ? 'wss' : 'ws' - var url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function bindEventHandler () { - if (isInitialized) return - - isInitialized = true - - my.onSocketOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - my.onSocketMessage(function (res) { - if (typeof res.data === 'string') { - var buffer = Buffer.from(res.data, 'base64') - proxy.push(buffer) - } else { - var reader = new FileReader() - reader.addEventListener('load', function () { - var data = reader.result - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - reader.readAsArrayBuffer(res.data) - } - }) - - my.onSocketClose(function () { - stream.end() - stream.destroy() - }) - - my.onSocketError(function (res) { - stream.destroy(res) - }) -} - -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - var websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - var url = buildUrl(opts, client) - my = opts.my - my.connectSocket({ - url: url, - protocols: websocketSubProtocol - }) - - proxy = buildProxy() - stream = duplexify.obj() - - bindEventHandler() - - return stream -} - -module.exports = buildStream +'use strict' + +var Transform = require('readable-stream').Transform +var duplexify = require('duplexify') + +/* global FileReader */ +var my +var proxy +var stream +var isInitialized = false + +function buildProxy () { + var proxy = new Transform() + proxy._write = function (chunk, encoding, next) { + my.sendSocketMessage({ + data: chunk.buffer, + success: function () { + next() + }, + fail: function () { + next(new Error()) + } + }) + } + proxy._flush = function socketEnd (done) { + my.closeSocket({ + success: function () { + done() + } + }) + } + + return proxy +} + +function setDefaultOpts (opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } +} + +function buildUrl (opts, client) { + var protocol = opts.protocol === 'alis' ? 'wss' : 'ws' + var url = protocol + '://' + opts.hostname + opts.path + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path + } + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function bindEventHandler () { + if (isInitialized) return + + isInitialized = true + + my.onSocketOpen(function () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + my.onSocketMessage(function (res) { + if (typeof res.data === 'string') { + var buffer = Buffer.from(res.data, 'base64') + proxy.push(buffer) + } else { + var reader = new FileReader() + reader.addEventListener('load', function () { + var data = reader.result + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + reader.readAsArrayBuffer(res.data) + } + }) + + my.onSocketClose(function () { + stream.end() + stream.destroy() + }) + + my.onSocketError(function (res) { + stream.destroy(res) + }) +} + +function buildStream (client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + var websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + var url = buildUrl(opts, client) + my = opts.my + my.connectSocket({ + url: url, + protocols: websocketSubProtocol + }) + + proxy = buildProxy() + stream = duplexify.obj() + + bindEventHandler() + + return stream +} + +module.exports = buildStream diff --git a/lib/connect/index.js b/lib/connect/index.js index 9fc151c75..97e7b4c15 100644 --- a/lib/connect/index.js +++ b/lib/connect/index.js @@ -1,164 +1,164 @@ -'use strict' - -var MqttClient = require('../client') -var Store = require('../store') -var url = require('url') -var xtend = require('xtend') -var debug = require('debug')('mqttjs') - -var protocols = {} - -// eslint-disable-next-line camelcase -if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { - protocols.mqtt = require('./tcp') - protocols.tcp = require('./tcp') - protocols.ssl = require('./tls') - protocols.tls = require('./tls') - protocols.mqtts = require('./tls') -} else { - protocols.wx = require('./wx') - protocols.wxs = require('./wx') - - protocols.ali = require('./ali') - protocols.alis = require('./ali') -} - -protocols.ws = require('./ws') -protocols.wss = require('./ws') - -/** - * Parse the auth attribute and merge username and password in the options object. - * - * @param {Object} [opts] option object - */ -function parseAuthOptions (opts) { - var matches - if (opts.auth) { - matches = opts.auth.match(/^(.+):(.+)$/) - if (matches) { - opts.username = matches[1] - opts.password = matches[2] - } else { - opts.username = opts.auth - } - } -} - -/** - * connect - connect to an MQTT broker. - * - * @param {String} [brokerUrl] - url of the broker, optional - * @param {Object} opts - see MqttClient#constructor - */ -function connect (brokerUrl, opts) { - debug('connecting to an MQTT broker...') - if ((typeof brokerUrl === 'object') && !opts) { - opts = brokerUrl - brokerUrl = null - } - - opts = opts || {} - - if (brokerUrl) { - var parsed = url.parse(brokerUrl, true) - if (parsed.port != null) { - parsed.port = Number(parsed.port) - } - - opts = xtend(parsed, opts) - - if (opts.protocol === null) { - throw new Error('Missing protocol') - } - - opts.protocol = opts.protocol.replace(/:$/, '') - } - - // merge in the auth options if supplied - parseAuthOptions(opts) - - // support clientId passed in the query string of the url - if (opts.query && typeof opts.query.clientId === 'string') { - opts.clientId = opts.query.clientId - } - - if (opts.cert && opts.key) { - if (opts.protocol) { - if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { - switch (opts.protocol) { - case 'mqtt': - opts.protocol = 'mqtts' - break - case 'ws': - opts.protocol = 'wss' - break - case 'wx': - opts.protocol = 'wxs' - break - case 'ali': - opts.protocol = 'alis' - break - default: - throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') - } - } - } else { - // A cert and key was provided, however no protocol was specified, so we will throw an error. - throw new Error('Missing secure protocol key') - } - } - - if (!protocols[opts.protocol]) { - var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 - opts.protocol = [ - 'mqtt', - 'mqtts', - 'ws', - 'wss', - 'wx', - 'wxs', - 'ali', - 'alis' - ].filter(function (key, index) { - if (isSecure && index % 2 === 0) { - // Skip insecure protocols when requesting a secure one. - return false - } - return (typeof protocols[key] === 'function') - })[0] - } - - if (opts.clean === false && !opts.clientId) { - throw new Error('Missing clientId for unclean clients') - } - - if (opts.protocol) { - opts.defaultProtocol = opts.protocol - } - - function wrapper (client) { - if (opts.servers) { - if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { - client._reconnectCount = 0 - } - - opts.host = opts.servers[client._reconnectCount].host - opts.port = opts.servers[client._reconnectCount].port - opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) - opts.hostname = opts.host - - client._reconnectCount++ - } - - debug('calling streambuilder for', opts.protocol) - return protocols[opts.protocol](client, opts) - } - var client = new MqttClient(wrapper, opts) - client.on('error', function () { /* Automatically set up client error handling */ }) - return client -} - -module.exports = connect -module.exports.connect = connect -module.exports.MqttClient = MqttClient -module.exports.Store = Store +'use strict' + +var MqttClient = require('../client') +var Store = require('../store') +var url = require('url') +var xtend = require('xtend') +var debug = require('debug')('mqttjs') + +var protocols = {} + +// eslint-disable-next-line camelcase +if ((typeof process !== 'undefined' && process.title !== 'browser') || typeof __webpack_require__ !== 'function') { + protocols.mqtt = require('./tcp') + protocols.tcp = require('./tcp') + protocols.ssl = require('./tls') + protocols.tls = require('./tls') + protocols.mqtts = require('./tls') +} else { + protocols.wx = require('./wx') + protocols.wxs = require('./wx') + + protocols.ali = require('./ali') + protocols.alis = require('./ali') +} + +protocols.ws = require('./ws') +protocols.wss = require('./ws') + +/** + * Parse the auth attribute and merge username and password in the options object. + * + * @param {Object} [opts] option object + */ +function parseAuthOptions (opts) { + var matches + if (opts.auth) { + matches = opts.auth.match(/^(.+):(.+)$/) + if (matches) { + opts.username = matches[1] + opts.password = matches[2] + } else { + opts.username = opts.auth + } + } +} + +/** + * connect - connect to an MQTT broker. + * + * @param {String} [brokerUrl] - url of the broker, optional + * @param {Object} opts - see MqttClient#constructor + */ +function connect (brokerUrl, opts) { + debug('connecting to an MQTT broker...') + if ((typeof brokerUrl === 'object') && !opts) { + opts = brokerUrl + brokerUrl = null + } + + opts = opts || {} + + if (brokerUrl) { + var parsed = url.parse(brokerUrl, true) + if (parsed.port != null) { + parsed.port = Number(parsed.port) + } + + opts = xtend(parsed, opts) + + if (opts.protocol === null) { + throw new Error('Missing protocol') + } + + opts.protocol = opts.protocol.replace(/:$/, '') + } + + // merge in the auth options if supplied + parseAuthOptions(opts) + + // support clientId passed in the query string of the url + if (opts.query && typeof opts.query.clientId === 'string') { + opts.clientId = opts.query.clientId + } + + if (opts.cert && opts.key) { + if (opts.protocol) { + if (['mqtts', 'wss', 'wxs', 'alis'].indexOf(opts.protocol) === -1) { + switch (opts.protocol) { + case 'mqtt': + opts.protocol = 'mqtts' + break + case 'ws': + opts.protocol = 'wss' + break + case 'wx': + opts.protocol = 'wxs' + break + case 'ali': + opts.protocol = 'alis' + break + default: + throw new Error('Unknown protocol for secure connection: "' + opts.protocol + '"!') + } + } + } else { + // A cert and key was provided, however no protocol was specified, so we will throw an error. + throw new Error('Missing secure protocol key') + } + } + + if (!protocols[opts.protocol]) { + var isSecure = ['mqtts', 'wss'].indexOf(opts.protocol) !== -1 + opts.protocol = [ + 'mqtt', + 'mqtts', + 'ws', + 'wss', + 'wx', + 'wxs', + 'ali', + 'alis' + ].filter(function (key, index) { + if (isSecure && index % 2 === 0) { + // Skip insecure protocols when requesting a secure one. + return false + } + return (typeof protocols[key] === 'function') + })[0] + } + + if (opts.clean === false && !opts.clientId) { + throw new Error('Missing clientId for unclean clients') + } + + if (opts.protocol) { + opts.defaultProtocol = opts.protocol + } + + function wrapper (client) { + if (opts.servers) { + if (!client._reconnectCount || client._reconnectCount === opts.servers.length) { + client._reconnectCount = 0 + } + + opts.host = opts.servers[client._reconnectCount].host + opts.port = opts.servers[client._reconnectCount].port + opts.protocol = (!opts.servers[client._reconnectCount].protocol ? opts.defaultProtocol : opts.servers[client._reconnectCount].protocol) + opts.hostname = opts.host + + client._reconnectCount++ + } + + debug('calling streambuilder for', opts.protocol) + return protocols[opts.protocol](client, opts) + } + var client = new MqttClient(wrapper, opts) + client.on('error', function () { /* Automatically set up client error handling */ }) + return client +} + +module.exports = connect +module.exports.connect = connect +module.exports.MqttClient = MqttClient +module.exports.Store = Store diff --git a/lib/connect/tcp.js b/lib/connect/tcp.js index 3fe2c0922..9912102eb 100644 --- a/lib/connect/tcp.js +++ b/lib/connect/tcp.js @@ -1,21 +1,21 @@ -'use strict' -var net = require('net') -var debug = require('debug')('mqttjs:tcp') - -/* - variables port and host can be removed since - you have all required information in opts object -*/ -function streamBuilder (client, opts) { - var port, host - opts.port = opts.port || 1883 - opts.hostname = opts.hostname || opts.host || 'localhost' - - port = opts.port - host = opts.hostname - - debug('port %d and host %s', port, host) - return net.createConnection(port, host) -} - -module.exports = streamBuilder +'use strict' +var net = require('net') +var debug = require('debug')('mqttjs:tcp') + +/* + variables port and host can be removed since + you have all required information in opts object +*/ +function streamBuilder (client, opts) { + var port, host + opts.port = opts.port || 1883 + opts.hostname = opts.hostname || opts.host || 'localhost' + + port = opts.port + host = opts.hostname + + debug('port %d and host %s', port, host) + return net.createConnection(port, host) +} + +module.exports = streamBuilder diff --git a/lib/connect/tls.js b/lib/connect/tls.js index 226bff8b3..aac296666 100644 --- a/lib/connect/tls.js +++ b/lib/connect/tls.js @@ -1,45 +1,45 @@ -'use strict' -var tls = require('tls') -var debug = require('debug')('mqttjs:tls') - -function buildBuilder (mqttClient, opts) { - var connection - opts.port = opts.port || 8883 - opts.host = opts.hostname || opts.host || 'localhost' - opts.servername = opts.host - - opts.rejectUnauthorized = opts.rejectUnauthorized !== false - - delete opts.path - - debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) - - connection = tls.connect(opts) - /* eslint no-use-before-define: [2, "nofunc"] */ - connection.on('secureConnect', function () { - if (opts.rejectUnauthorized && !connection.authorized) { - connection.emit('error', new Error('TLS not authorized')) - } else { - connection.removeListener('error', handleTLSerrors) - } - }) - - function handleTLSerrors (err) { - // How can I get verify this error is a tls error? - if (opts.rejectUnauthorized) { - mqttClient.emit('error', err) - } - - // close this connection to match the behaviour of net - // otherwise all we get is an error from the connection - // and close event doesn't fire. This is a work around - // to enable the reconnect code to work the same as with - // net.createConnection - connection.end() - } - - connection.on('error', handleTLSerrors) - return connection -} - -module.exports = buildBuilder +'use strict' +var tls = require('tls') +var debug = require('debug')('mqttjs:tls') + +function buildBuilder (mqttClient, opts) { + var connection + opts.port = opts.port || 8883 + opts.host = opts.hostname || opts.host || 'localhost' + opts.servername = opts.host + + opts.rejectUnauthorized = opts.rejectUnauthorized !== false + + delete opts.path + + debug('port %d host %s rejectUnauthorized %b', opts.port, opts.host, opts.rejectUnauthorized) + + connection = tls.connect(opts) + /* eslint no-use-before-define: [2, "nofunc"] */ + connection.on('secureConnect', function () { + if (opts.rejectUnauthorized && !connection.authorized) { + connection.emit('error', new Error('TLS not authorized')) + } else { + connection.removeListener('error', handleTLSerrors) + } + }) + + function handleTLSerrors (err) { + // How can I get verify this error is a tls error? + if (opts.rejectUnauthorized) { + mqttClient.emit('error', err) + } + + // close this connection to match the behaviour of net + // otherwise all we get is an error from the connection + // and close event doesn't fire. This is a work around + // to enable the reconnect code to work the same as with + // net.createConnection + connection.end() + } + + connection.on('error', handleTLSerrors) + return connection +} + +module.exports = buildBuilder diff --git a/lib/connect/ws.js b/lib/connect/ws.js index 18646a5a1..5c1d2c691 100644 --- a/lib/connect/ws.js +++ b/lib/connect/ws.js @@ -1,256 +1,256 @@ -'use strict' - -const WS = require('ws') -const debug = require('debug')('mqttjs:ws') -const duplexify = require('duplexify') -const Transform = require('readable-stream').Transform - -let WSS_OPTIONS = [ - 'rejectUnauthorized', - 'ca', - 'cert', - 'key', - 'pfx', - 'passphrase' -] -// eslint-disable-next-line camelcase -const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' -function buildUrl (opts, client) { - let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function setDefaultOpts (opts) { - let options = opts - if (!opts.hostname) { - options.hostname = 'localhost' - } - if (!opts.port) { - if (opts.protocol === 'wss') { - options.port = 443 - } else { - options.port = 80 - } - } - if (!opts.path) { - options.path = '/' - } - - if (!opts.wsOptions) { - options.wsOptions = {} - } - if (!IS_BROWSER && opts.protocol === 'wss') { - // Add cert/key/ca etc options - WSS_OPTIONS.forEach(function (prop) { - if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { - options.wsOptions[prop] = opts[prop] - } - }) - } - - return options -} - -function setDefaultBrowserOpts (opts) { - let options = setDefaultOpts(opts) - - if (!options.hostname) { - options.hostname = options.host - } - - if (!options.hostname) { - // Throwing an error in a Web Worker if no `hostname` is given, because we - // can not determine the `hostname` automatically. If connecting to - // localhost, please supply the `hostname` as an argument. - if (typeof (document) === 'undefined') { - throw new Error('Could not determine host. Specify host manually.') - } - const parsed = new URL(document.URL) - options.hostname = parsed.hostname - - if (!options.port) { - options.port = parsed.port - } - } - - // objectMode should be defined for logic - if (options.objectMode === undefined) { - options.objectMode = !(options.binary === true || options.binary === undefined) - } - - return options -} - -function createWebSocket (client, url, opts) { - debug('createWebSocket') - debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) - let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) - return socket -} - -function createBrowserWebSocket (client, opts) { - const websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - let url = buildUrl(opts, client) - /* global WebSocket */ - let socket = new WebSocket(url, [websocketSubProtocol]) - socket.binaryType = 'arraybuffer' - return socket -} - -function streamBuilder (client, opts) { - debug('streamBuilder') - let options = setDefaultOpts(opts) - const url = buildUrl(options, client) - let socket = createWebSocket(client, url, options) - let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) - webSocketStream.url = url - socket.on('close', () => { webSocketStream.destroy() }) - return webSocketStream -} - -function browserStreamBuilder (client, opts) { - debug('browserStreamBuilder') - let stream - let options = setDefaultBrowserOpts(opts) - // sets the maximum socket buffer size before throttling - const bufferSize = options.browserBufferSize || 1024 * 512 - - const bufferTimeout = opts.browserBufferTimeout || 1000 - - const coerceToBuffer = !opts.objectMode - - let socket = createBrowserWebSocket(client, opts) - - let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) - - if (!opts.objectMode) { - proxy._writev = writev - } - proxy.on('close', () => { socket.close() }) - - const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') - - // was already open when passed in - if (socket.readyState === socket.OPEN) { - stream = proxy - } else { - stream = stream = duplexify(undefined, undefined, opts) - if (!opts.objectMode) { - stream._writev = writev - } - - if (eventListenerSupport) { - socket.addEventListener('open', onopen) - } else { - socket.onopen = onopen - } - } - - stream.socket = socket - - if (eventListenerSupport) { - socket.addEventListener('close', onclose) - socket.addEventListener('error', onerror) - socket.addEventListener('message', onmessage) - } else { - socket.onclose = onclose - socket.onerror = onerror - socket.onmessage = onmessage - } - - // methods for browserStreamBuilder - - function buildProxy (options, socketWrite, socketEnd) { - let proxy = new Transform({ - objectModeMode: options.objectMode - }) - - proxy._write = socketWrite - proxy._flush = socketEnd - - return proxy - } - - function onopen () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - } - - function onclose () { - stream.end() - stream.destroy() - } - - function onerror (err) { - stream.destroy(err) - } - - function onmessage (event) { - let data = event.data - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - } - - // this is to be enabled only if objectMode is false - function writev (chunks, cb) { - const buffers = new Array(chunks.length) - for (let i = 0; i < chunks.length; i++) { - if (typeof chunks[i].chunk === 'string') { - buffers[i] = Buffer.from(chunks[i], 'utf8') - } else { - buffers[i] = chunks[i].chunk - } - } - - this._write(Buffer.concat(buffers), 'binary', cb) - } - - function socketWriteBrowser (chunk, enc, next) { - if (socket.bufferedAmount > bufferSize) { - // throttle data until buffered amount is reduced. - setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) - } - - if (coerceToBuffer && typeof chunk === 'string') { - chunk = Buffer.from(chunk, 'utf8') - } - - try { - socket.send(chunk) - } catch (err) { - return next(err) - } - - next() - } - - function socketEndBrowser (done) { - socket.close() - done() - } - - // end methods for browserStreamBuilder - - return stream -} - -if (IS_BROWSER) { - module.exports = browserStreamBuilder -} else { - module.exports = streamBuilder -} +'use strict' + +const WS = require('ws') +const debug = require('debug')('mqttjs:ws') +const duplexify = require('duplexify') +const Transform = require('readable-stream').Transform + +let WSS_OPTIONS = [ + 'rejectUnauthorized', + 'ca', + 'cert', + 'key', + 'pfx', + 'passphrase' +] +// eslint-disable-next-line camelcase +const IS_BROWSER = (typeof process !== 'undefined' && process.title === 'browser') || typeof __webpack_require__ === 'function' +function buildUrl (opts, client) { + let url = opts.protocol + '://' + opts.hostname + ':' + opts.port + opts.path + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function setDefaultOpts (opts) { + let options = opts + if (!opts.hostname) { + options.hostname = 'localhost' + } + if (!opts.port) { + if (opts.protocol === 'wss') { + options.port = 443 + } else { + options.port = 80 + } + } + if (!opts.path) { + options.path = '/' + } + + if (!opts.wsOptions) { + options.wsOptions = {} + } + if (!IS_BROWSER && opts.protocol === 'wss') { + // Add cert/key/ca etc options + WSS_OPTIONS.forEach(function (prop) { + if (opts.hasOwnProperty(prop) && !opts.wsOptions.hasOwnProperty(prop)) { + options.wsOptions[prop] = opts[prop] + } + }) + } + + return options +} + +function setDefaultBrowserOpts (opts) { + let options = setDefaultOpts(opts) + + if (!options.hostname) { + options.hostname = options.host + } + + if (!options.hostname) { + // Throwing an error in a Web Worker if no `hostname` is given, because we + // can not determine the `hostname` automatically. If connecting to + // localhost, please supply the `hostname` as an argument. + if (typeof (document) === 'undefined') { + throw new Error('Could not determine host. Specify host manually.') + } + const parsed = new URL(document.URL) + options.hostname = parsed.hostname + + if (!options.port) { + options.port = parsed.port + } + } + + // objectMode should be defined for logic + if (options.objectMode === undefined) { + options.objectMode = !(options.binary === true || options.binary === undefined) + } + + return options +} + +function createWebSocket (client, url, opts) { + debug('createWebSocket') + debug('protocol: ' + opts.protocolId + ' ' + opts.protocolVersion) + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + debug('creating new Websocket for url: ' + url + ' and protocol: ' + websocketSubProtocol) + let socket = new WS(url, [websocketSubProtocol], opts.wsOptions) + return socket +} + +function createBrowserWebSocket (client, opts) { + const websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + let url = buildUrl(opts, client) + /* global WebSocket */ + let socket = new WebSocket(url, [websocketSubProtocol]) + socket.binaryType = 'arraybuffer' + return socket +} + +function streamBuilder (client, opts) { + debug('streamBuilder') + let options = setDefaultOpts(opts) + const url = buildUrl(options, client) + let socket = createWebSocket(client, url, options) + let webSocketStream = WS.createWebSocketStream(socket, options.wsOptions) + webSocketStream.url = url + socket.on('close', () => { webSocketStream.destroy() }) + return webSocketStream +} + +function browserStreamBuilder (client, opts) { + debug('browserStreamBuilder') + let stream + let options = setDefaultBrowserOpts(opts) + // sets the maximum socket buffer size before throttling + const bufferSize = options.browserBufferSize || 1024 * 512 + + const bufferTimeout = opts.browserBufferTimeout || 1000 + + const coerceToBuffer = !opts.objectMode + + let socket = createBrowserWebSocket(client, opts) + + let proxy = buildProxy(opts, socketWriteBrowser, socketEndBrowser) + + if (!opts.objectMode) { + proxy._writev = writev + } + proxy.on('close', () => { socket.close() }) + + const eventListenerSupport = (typeof socket.addEventListener !== 'undefined') + + // was already open when passed in + if (socket.readyState === socket.OPEN) { + stream = proxy + } else { + stream = stream = duplexify(undefined, undefined, opts) + if (!opts.objectMode) { + stream._writev = writev + } + + if (eventListenerSupport) { + socket.addEventListener('open', onopen) + } else { + socket.onopen = onopen + } + } + + stream.socket = socket + + if (eventListenerSupport) { + socket.addEventListener('close', onclose) + socket.addEventListener('error', onerror) + socket.addEventListener('message', onmessage) + } else { + socket.onclose = onclose + socket.onerror = onerror + socket.onmessage = onmessage + } + + // methods for browserStreamBuilder + + function buildProxy (options, socketWrite, socketEnd) { + let proxy = new Transform({ + objectModeMode: options.objectMode + }) + + proxy._write = socketWrite + proxy._flush = socketEnd + + return proxy + } + + function onopen () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + } + + function onclose () { + stream.end() + stream.destroy() + } + + function onerror (err) { + stream.destroy(err) + } + + function onmessage (event) { + let data = event.data + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + } + + // this is to be enabled only if objectMode is false + function writev (chunks, cb) { + const buffers = new Array(chunks.length) + for (let i = 0; i < chunks.length; i++) { + if (typeof chunks[i].chunk === 'string') { + buffers[i] = Buffer.from(chunks[i], 'utf8') + } else { + buffers[i] = chunks[i].chunk + } + } + + this._write(Buffer.concat(buffers), 'binary', cb) + } + + function socketWriteBrowser (chunk, enc, next) { + if (socket.bufferedAmount > bufferSize) { + // throttle data until buffered amount is reduced. + setTimeout(socketWriteBrowser, bufferTimeout, chunk, enc, next) + } + + if (coerceToBuffer && typeof chunk === 'string') { + chunk = Buffer.from(chunk, 'utf8') + } + + try { + socket.send(chunk) + } catch (err) { + return next(err) + } + + next() + } + + function socketEndBrowser (done) { + socket.close() + done() + } + + // end methods for browserStreamBuilder + + return stream +} + +if (IS_BROWSER) { + module.exports = browserStreamBuilder +} else { + module.exports = streamBuilder +} diff --git a/lib/connect/wx.js b/lib/connect/wx.js index 2b675079a..b9c7a0705 100644 --- a/lib/connect/wx.js +++ b/lib/connect/wx.js @@ -1,134 +1,134 @@ -'use strict' - -var Transform = require('readable-stream').Transform -var duplexify = require('duplexify') - -/* global wx */ -var socketTask -var proxy -var stream - -function buildProxy () { - var proxy = new Transform() - proxy._write = function (chunk, encoding, next) { - socketTask.send({ - data: chunk.buffer, - success: function () { - next() - }, - fail: function (errMsg) { - next(new Error(errMsg)) - } - }) - } - proxy._flush = function socketEnd (done) { - socketTask.close({ - success: function () { - done() - } - }) - } - - return proxy -} - -function setDefaultOpts (opts) { - if (!opts.hostname) { - opts.hostname = 'localhost' - } - if (!opts.path) { - opts.path = '/' - } - - if (!opts.wsOptions) { - opts.wsOptions = {} - } -} - -function buildUrl (opts, client) { - var protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' - var url = protocol + '://' + opts.hostname + opts.path - if (opts.port && opts.port !== 80 && opts.port !== 443) { - url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path - } - if (typeof (opts.transformWsUrl) === 'function') { - url = opts.transformWsUrl(url, opts, client) - } - return url -} - -function bindEventHandler () { - socketTask.onOpen(function () { - stream.setReadable(proxy) - stream.setWritable(proxy) - stream.emit('connect') - }) - - socketTask.onMessage(function (res) { - var data = res.data - - if (data instanceof ArrayBuffer) data = Buffer.from(data) - else data = Buffer.from(data, 'utf8') - proxy.push(data) - }) - - socketTask.onClose(function () { - stream.end() - stream.destroy() - }) - - socketTask.onError(function (res) { - stream.destroy(new Error(res.errMsg)) - }) -} - -function buildStream (client, opts) { - opts.hostname = opts.hostname || opts.host - - if (!opts.hostname) { - throw new Error('Could not determine host. Specify host manually.') - } - - var websocketSubProtocol = - (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) - ? 'mqttv3.1' - : 'mqtt' - - setDefaultOpts(opts) - - var url = buildUrl(opts, client) - socketTask = wx.connectSocket({ - url: url, - protocols: [websocketSubProtocol] - }) - - proxy = buildProxy() - stream = duplexify.obj() - stream._destroy = function (err, cb) { - socketTask.close({ - success: function () { - cb && cb(err) - } - }) - } - - var destroyRef = stream.destroy - stream.destroy = function () { - stream.destroy = destroyRef - - var self = this - setTimeout(function () { - socketTask.close({ - fail: function () { - self._destroy(new Error()) - } - }) - }, 0) - }.bind(stream) - - bindEventHandler() - - return stream -} - -module.exports = buildStream +'use strict' + +var Transform = require('readable-stream').Transform +var duplexify = require('duplexify') + +/* global wx */ +var socketTask +var proxy +var stream + +function buildProxy () { + var proxy = new Transform() + proxy._write = function (chunk, encoding, next) { + socketTask.send({ + data: chunk.buffer, + success: function () { + next() + }, + fail: function (errMsg) { + next(new Error(errMsg)) + } + }) + } + proxy._flush = function socketEnd (done) { + socketTask.close({ + success: function () { + done() + } + }) + } + + return proxy +} + +function setDefaultOpts (opts) { + if (!opts.hostname) { + opts.hostname = 'localhost' + } + if (!opts.path) { + opts.path = '/' + } + + if (!opts.wsOptions) { + opts.wsOptions = {} + } +} + +function buildUrl (opts, client) { + var protocol = opts.protocol === 'wxs' ? 'wss' : 'ws' + var url = protocol + '://' + opts.hostname + opts.path + if (opts.port && opts.port !== 80 && opts.port !== 443) { + url = protocol + '://' + opts.hostname + ':' + opts.port + opts.path + } + if (typeof (opts.transformWsUrl) === 'function') { + url = opts.transformWsUrl(url, opts, client) + } + return url +} + +function bindEventHandler () { + socketTask.onOpen(function () { + stream.setReadable(proxy) + stream.setWritable(proxy) + stream.emit('connect') + }) + + socketTask.onMessage(function (res) { + var data = res.data + + if (data instanceof ArrayBuffer) data = Buffer.from(data) + else data = Buffer.from(data, 'utf8') + proxy.push(data) + }) + + socketTask.onClose(function () { + stream.end() + stream.destroy() + }) + + socketTask.onError(function (res) { + stream.destroy(new Error(res.errMsg)) + }) +} + +function buildStream (client, opts) { + opts.hostname = opts.hostname || opts.host + + if (!opts.hostname) { + throw new Error('Could not determine host. Specify host manually.') + } + + var websocketSubProtocol = + (opts.protocolId === 'MQIsdp') && (opts.protocolVersion === 3) + ? 'mqttv3.1' + : 'mqtt' + + setDefaultOpts(opts) + + var url = buildUrl(opts, client) + socketTask = wx.connectSocket({ + url: url, + protocols: [websocketSubProtocol] + }) + + proxy = buildProxy() + stream = duplexify.obj() + stream._destroy = function (err, cb) { + socketTask.close({ + success: function () { + cb && cb(err) + } + }) + } + + var destroyRef = stream.destroy + stream.destroy = function () { + stream.destroy = destroyRef + + var self = this + setTimeout(function () { + socketTask.close({ + fail: function () { + self._destroy(new Error()) + } + }) + }, 0) + }.bind(stream) + + bindEventHandler() + + return stream +} + +module.exports = buildStream diff --git a/lib/default-message-id-provider.js b/lib/default-message-id-provider.js index d1bcc9ed0..c0a953f3f 100644 --- a/lib/default-message-id-provider.js +++ b/lib/default-message-id-provider.js @@ -1,69 +1,69 @@ -'use strict' - -/** - * DefaultMessageAllocator constructor - * @constructor - */ -function DefaultMessageIdProvider () { - if (!(this instanceof DefaultMessageIdProvider)) { - return new DefaultMessageIdProvider() - } - - /** - * MessageIDs starting with 1 - * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 - */ - this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) -} - -/** - * allocate - * - * Get the next messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.allocate = function () { - // id becomes current state of this.nextId and increments afterwards - var id = this.nextId++ - // Ensure 16 bit unsigned int (max 65535, nextId got one higher) - if (this.nextId === 65536) { - this.nextId = 1 - } - return id -} - -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -DefaultMessageIdProvider.prototype.getLastAllocated = function () { - return (this.nextId === 1) ? 65535 : (this.nextId - 1) -} - -/** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ -DefaultMessageIdProvider.prototype.register = function (messageId) { - return true -} - -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -DefaultMessageIdProvider.prototype.deallocate = function (messageId) { -} - -/** - * clear - * Deallocate all messageIds. - */ -DefaultMessageIdProvider.prototype.clear = function () { -} - -module.exports = DefaultMessageIdProvider +'use strict' + +/** + * DefaultMessageAllocator constructor + * @constructor + */ +function DefaultMessageIdProvider () { + if (!(this instanceof DefaultMessageIdProvider)) { + return new DefaultMessageIdProvider() + } + + /** + * MessageIDs starting with 1 + * ensure that nextId is min. 1, see https://github.com/mqttjs/MQTT.js/issues/810 + */ + this.nextId = Math.max(1, Math.floor(Math.random() * 65535)) +} + +/** + * allocate + * + * Get the next messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.allocate = function () { + // id becomes current state of this.nextId and increments afterwards + var id = this.nextId++ + // Ensure 16 bit unsigned int (max 65535, nextId got one higher) + if (this.nextId === 65536) { + this.nextId = 1 + } + return id +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +DefaultMessageIdProvider.prototype.getLastAllocated = function () { + return (this.nextId === 1) ? 65535 : (this.nextId - 1) +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +DefaultMessageIdProvider.prototype.register = function (messageId) { + return true +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +DefaultMessageIdProvider.prototype.deallocate = function (messageId) { +} + +/** + * clear + * Deallocate all messageIds. + */ +DefaultMessageIdProvider.prototype.clear = function () { +} + +module.exports = DefaultMessageIdProvider diff --git a/lib/store.js b/lib/store.js index 37809750b..efbfabf09 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,128 +1,128 @@ -'use strict' - -/** - * Module dependencies - */ -var xtend = require('xtend') - -var Readable = require('readable-stream').Readable -var streamsOpts = { objectMode: true } -var defaultStoreOptions = { - clean: true -} - -/** - * In-memory implementation of the message store - * This can actually be saved into files. - * - * @param {Object} [options] - store options - */ -function Store (options) { - if (!(this instanceof Store)) { - return new Store(options) - } - - this.options = options || {} - - // Defaults - this.options = xtend(defaultStoreOptions, options) - - this._inflights = new Map() -} - -/** - * Adds a packet to the store, a packet is - * anything that has a messageId property. - * - */ -Store.prototype.put = function (packet, cb) { - this._inflights.set(packet.messageId, packet) - - if (cb) { - cb() - } - - return this -} - -/** - * Creates a stream with all the packets in the store - * - */ -Store.prototype.createStream = function () { - var stream = new Readable(streamsOpts) - var destroyed = false - var values = [] - var i = 0 - - this._inflights.forEach(function (value, key) { - values.push(value) - }) - - stream._read = function () { - if (!destroyed && i < values.length) { - this.push(values[i++]) - } else { - this.push(null) - } - } - - stream.destroy = function () { - if (destroyed) { - return - } - - var self = this - - destroyed = true - - setTimeout(function () { - self.emit('close') - }, 0) - } - - return stream -} - -/** - * deletes a packet from the store. - */ -Store.prototype.del = function (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - this._inflights.delete(packet.messageId) - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this -} - -/** - * get a packet from the store. - */ -Store.prototype.get = function (packet, cb) { - packet = this._inflights.get(packet.messageId) - if (packet) { - cb(null, packet) - } else if (cb) { - cb(new Error('missing packet')) - } - - return this -} - -/** - * Close the store - */ -Store.prototype.close = function (cb) { - if (this.options.clean) { - this._inflights = null - } - if (cb) { - cb() - } -} - -module.exports = Store +'use strict' + +/** + * Module dependencies + */ +var xtend = require('xtend') + +var Readable = require('readable-stream').Readable +var streamsOpts = { objectMode: true } +var defaultStoreOptions = { + clean: true +} + +/** + * In-memory implementation of the message store + * This can actually be saved into files. + * + * @param {Object} [options] - store options + */ +function Store (options) { + if (!(this instanceof Store)) { + return new Store(options) + } + + this.options = options || {} + + // Defaults + this.options = xtend(defaultStoreOptions, options) + + this._inflights = new Map() +} + +/** + * Adds a packet to the store, a packet is + * anything that has a messageId property. + * + */ +Store.prototype.put = function (packet, cb) { + this._inflights.set(packet.messageId, packet) + + if (cb) { + cb() + } + + return this +} + +/** + * Creates a stream with all the packets in the store + * + */ +Store.prototype.createStream = function () { + var stream = new Readable(streamsOpts) + var destroyed = false + var values = [] + var i = 0 + + this._inflights.forEach(function (value, key) { + values.push(value) + }) + + stream._read = function () { + if (!destroyed && i < values.length) { + this.push(values[i++]) + } else { + this.push(null) + } + } + + stream.destroy = function () { + if (destroyed) { + return + } + + var self = this + + destroyed = true + + setTimeout(function () { + self.emit('close') + }, 0) + } + + return stream +} + +/** + * deletes a packet from the store. + */ +Store.prototype.del = function (packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + this._inflights.delete(packet.messageId) + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this +} + +/** + * get a packet from the store. + */ +Store.prototype.get = function (packet, cb) { + packet = this._inflights.get(packet.messageId) + if (packet) { + cb(null, packet) + } else if (cb) { + cb(new Error('missing packet')) + } + + return this +} + +/** + * Close the store + */ +Store.prototype.close = function (cb) { + if (this.options.clean) { + this._inflights = null + } + if (cb) { + cb() + } +} + +module.exports = Store diff --git a/lib/topic-alias-send.js b/lib/topic-alias-send.js index 71b10468a..f3abf2084 100644 --- a/lib/topic-alias-send.js +++ b/lib/topic-alias-send.js @@ -1,93 +1,93 @@ -'use strict' - -/** - * Module dependencies - */ -var LruMap = require('collections/lru-map') -var NumberAllocator = require('number-allocator').NumberAllocator - -/** - * Topic Alias sending manager - * This holds both topic to alias and alias to topic map - * @param {Number} [max] - topic alias maximum entries - */ -function TopicAliasSend (max) { - if (!(this instanceof TopicAliasSend)) { - return new TopicAliasSend(max) - } - - if (max > 0) { - this.aliasToTopic = new LruMap() - this.topicToAlias = {} - this.numberAllocator = new NumberAllocator(1, max) - this.max = max - this.length = 0 - } -} - -/** - * Insert or update topic - alias entry. - * @param {String} [topic] - topic - * @param {Number} [alias] - topic alias - * @returns {Boolean} - if success return true otherwise false - */ -TopicAliasSend.prototype.put = function (topic, alias) { - if (alias === 0 || alias > this.max) { - return false - } - const entry = this.aliasToTopic.get(alias) - if (entry) { - delete this.topicToAlias[entry.topic] - } - this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) - this.topicToAlias[topic] = alias - this.numberAllocator.use(alias) - this.length = this.aliasToTopic.length - return true -} - -/** - * Get topic by alias - * @param {Number} [alias] - topic alias - * @returns {String} - if mapped topic exists return topic, otherwise return undefined - */ -TopicAliasSend.prototype.getTopicByAlias = function (alias) { - const entry = this.aliasToTopic.get(alias) - if (typeof entry === 'undefined') return entry - return entry.topic -} - -/** - * Get topic by alias - * @param {String} [topic] - topic - * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined - */ -TopicAliasSend.prototype.getAliasByTopic = function (topic) { - const alias = this.topicToAlias[topic] - if (typeof alias !== 'undefined') { - this.aliasToTopic.get(alias) // LRU update - } - return alias -} - -/** - * Clear all entries - */ -TopicAliasSend.prototype.clear = function () { - this.aliasToTopic.clear() - this.topicToAlias = {} - this.numberAllocator.clear() - this.length = 0 -} - -/** - * Get Least Recently Used (LRU) topic alias - * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias - */ -TopicAliasSend.prototype.getLruAlias = function () { - const alias = this.numberAllocator.firstVacant() - if (alias) return alias - return this.aliasToTopic.min().alias -} - -module.exports = TopicAliasSend +'use strict' + +/** + * Module dependencies + */ +var LruMap = require('collections/lru-map') +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * Topic Alias sending manager + * This holds both topic to alias and alias to topic map + * @param {Number} [max] - topic alias maximum entries + */ +function TopicAliasSend (max) { + if (!(this instanceof TopicAliasSend)) { + return new TopicAliasSend(max) + } + + if (max > 0) { + this.aliasToTopic = new LruMap() + this.topicToAlias = {} + this.numberAllocator = new NumberAllocator(1, max) + this.max = max + this.length = 0 + } +} + +/** + * Insert or update topic - alias entry. + * @param {String} [topic] - topic + * @param {Number} [alias] - topic alias + * @returns {Boolean} - if success return true otherwise false + */ +TopicAliasSend.prototype.put = function (topic, alias) { + if (alias === 0 || alias > this.max) { + return false + } + const entry = this.aliasToTopic.get(alias) + if (entry) { + delete this.topicToAlias[entry.topic] + } + this.aliasToTopic.set(alias, {'topic': topic, 'alias': alias}) + this.topicToAlias[topic] = alias + this.numberAllocator.use(alias) + this.length = this.aliasToTopic.length + return true +} + +/** + * Get topic by alias + * @param {Number} [alias] - topic alias + * @returns {String} - if mapped topic exists return topic, otherwise return undefined + */ +TopicAliasSend.prototype.getTopicByAlias = function (alias) { + const entry = this.aliasToTopic.get(alias) + if (typeof entry === 'undefined') return entry + return entry.topic +} + +/** + * Get topic by alias + * @param {String} [topic] - topic + * @returns {Number} - if mapped topic exists return topic alias, otherwise return undefined + */ +TopicAliasSend.prototype.getAliasByTopic = function (topic) { + const alias = this.topicToAlias[topic] + if (typeof alias !== 'undefined') { + this.aliasToTopic.get(alias) // LRU update + } + return alias +} + +/** + * Clear all entries + */ +TopicAliasSend.prototype.clear = function () { + this.aliasToTopic.clear() + this.topicToAlias = {} + this.numberAllocator.clear() + this.length = 0 +} + +/** + * Get Least Recently Used (LRU) topic alias + * @returns {Number} - if vacant alias exists then return it, otherwise then return LRU alias + */ +TopicAliasSend.prototype.getLruAlias = function () { + const alias = this.numberAllocator.firstVacant() + if (alias) return alias + return this.aliasToTopic.min().alias +} + +module.exports = TopicAliasSend diff --git a/lib/unique-message-id-provider.js b/lib/unique-message-id-provider.js index 20e59977f..6ffd4bde6 100644 --- a/lib/unique-message-id-provider.js +++ b/lib/unique-message-id-provider.js @@ -1,65 +1,65 @@ -'use strict' - -var NumberAllocator = require('number-allocator').NumberAllocator - -/** - * UniqueMessageAllocator constructor - * @constructor - */ -function UniqueMessageIdProvider () { - if (!(this instanceof UniqueMessageIdProvider)) { - return new UniqueMessageIdProvider() - } - - this.numberAllocator = new NumberAllocator(1, 65535) -} - -/** - * allocate - * - * Get the next messageId. - * @return if messageId is fully allocated then return null, - * otherwise return the smallest usable unsigned int messageId. - */ -UniqueMessageIdProvider.prototype.allocate = function () { - this.lastId = this.numberAllocator.alloc() - return this.lastId -} - -/** - * getLastAllocated - * Get the last allocated messageId. - * @return unsigned int - */ -UniqueMessageIdProvider.prototype.getLastAllocated = function () { - return this.lastId -} - -/** - * register - * Register messageId. If success return true, otherwise return false. - * @param { unsigned int } - messageId to register, - * @return boolean - */ -UniqueMessageIdProvider.prototype.register = function (messageId) { - return this.numberAllocator.use(messageId) -} - -/** - * deallocate - * Deallocate messageId. - * @param { unsigned int } - messageId to deallocate, - */ -UniqueMessageIdProvider.prototype.deallocate = function (messageId) { - this.numberAllocator.free(messageId) -} - -/** - * clear - * Deallocate all messageIds. - */ -UniqueMessageIdProvider.prototype.clear = function () { - this.numberAllocator.clear() -} - -module.exports = UniqueMessageIdProvider +'use strict' + +var NumberAllocator = require('number-allocator').NumberAllocator + +/** + * UniqueMessageAllocator constructor + * @constructor + */ +function UniqueMessageIdProvider () { + if (!(this instanceof UniqueMessageIdProvider)) { + return new UniqueMessageIdProvider() + } + + this.numberAllocator = new NumberAllocator(1, 65535) +} + +/** + * allocate + * + * Get the next messageId. + * @return if messageId is fully allocated then return null, + * otherwise return the smallest usable unsigned int messageId. + */ +UniqueMessageIdProvider.prototype.allocate = function () { + this.lastId = this.numberAllocator.alloc() + return this.lastId +} + +/** + * getLastAllocated + * Get the last allocated messageId. + * @return unsigned int + */ +UniqueMessageIdProvider.prototype.getLastAllocated = function () { + return this.lastId +} + +/** + * register + * Register messageId. If success return true, otherwise return false. + * @param { unsigned int } - messageId to register, + * @return boolean + */ +UniqueMessageIdProvider.prototype.register = function (messageId) { + return this.numberAllocator.use(messageId) +} + +/** + * deallocate + * Deallocate messageId. + * @param { unsigned int } - messageId to deallocate, + */ +UniqueMessageIdProvider.prototype.deallocate = function (messageId) { + this.numberAllocator.free(messageId) +} + +/** + * clear + * Deallocate all messageIds. + */ +UniqueMessageIdProvider.prototype.clear = function () { + this.numberAllocator.clear() +} + +module.exports = UniqueMessageIdProvider diff --git a/lib/validations.js b/lib/validations.js index 452e3ba1a..1a3277901 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -1,52 +1,52 @@ -'use strict' - -/** - * Validate a topic to see if it's valid or not. - * A topic is valid if it follow below rules: - * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' - * - Rule #2: Part `#` must be located at the end of the mailbox - * - * @param {String} topic - A topic - * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. - */ -function validateTopic (topic) { - var parts = topic.split('/') - - for (var i = 0; i < parts.length; i++) { - if (parts[i] === '+') { - continue - } - - if (parts[i] === '#') { - // for Rule #2 - return i === parts.length - 1 - } - - if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { - return false - } - } - - return true -} - -/** - * Validate an array of topics to see if any of them is valid or not - * @param {Array} topics - Array of topics - * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one - */ -function validateTopics (topics) { - if (topics.length === 0) { - return 'empty_topic_list' - } - for (var i = 0; i < topics.length; i++) { - if (!validateTopic(topics[i])) { - return topics[i] - } - } - return null -} - -module.exports = { - validateTopics: validateTopics -} +'use strict' + +/** + * Validate a topic to see if it's valid or not. + * A topic is valid if it follow below rules: + * - Rule #1: If any part of the topic is not `+` or `#`, then it must not contain `+` and '#' + * - Rule #2: Part `#` must be located at the end of the mailbox + * + * @param {String} topic - A topic + * @returns {Boolean} If the topic is valid, returns true. Otherwise, returns false. + */ +function validateTopic (topic) { + var parts = topic.split('/') + + for (var i = 0; i < parts.length; i++) { + if (parts[i] === '+') { + continue + } + + if (parts[i] === '#') { + // for Rule #2 + return i === parts.length - 1 + } + + if (parts[i].indexOf('+') !== -1 || parts[i].indexOf('#') !== -1) { + return false + } + } + + return true +} + +/** + * Validate an array of topics to see if any of them is valid or not + * @param {Array} topics - Array of topics + * @returns {String} If the topics is valid, returns null. Otherwise, returns the invalid one + */ +function validateTopics (topics) { + if (topics.length === 0) { + return 'empty_topic_list' + } + for (var i = 0; i < topics.length; i++) { + if (!validateTopic(topics[i])) { + return topics[i] + } + } + return null +} + +module.exports = { + validateTopics: validateTopics +} diff --git a/mqtt.js b/mqtt.js index 56cd6f04e..c8b94fda1 100644 --- a/mqtt.js +++ b/mqtt.js @@ -1,21 +1,21 @@ -/* - * Copyright (c) 2015-2015 MQTT.js contributors. - * Copyright (c) 2011-2014 Adam Rudd. - * - * See LICENSE for more information - */ - -var MqttClient = require('./lib/client') -var connect = require('./lib/connect') -var Store = require('./lib/store') -var DefaultMessageIdProvider = require('./lib/default-message-id-provider') -var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') - -module.exports.connect = connect - -// Expose MqttClient -module.exports.MqttClient = MqttClient -module.exports.Client = MqttClient -module.exports.Store = Store -module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider -module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider +/* + * Copyright (c) 2015-2015 MQTT.js contributors. + * Copyright (c) 2011-2014 Adam Rudd. + * + * See LICENSE for more information + */ + +var MqttClient = require('./lib/client') +var connect = require('./lib/connect') +var Store = require('./lib/store') +var DefaultMessageIdProvider = require('./lib/default-message-id-provider') +var UniqueMessageIdProvider = require('./lib/unique-message-id-provider') + +module.exports.connect = connect + +// Expose MqttClient +module.exports.MqttClient = MqttClient +module.exports.Client = MqttClient +module.exports.Store = Store +module.exports.DefaultMessageIdProvider = DefaultMessageIdProvider +module.exports.UniqueMessageIdProvider = UniqueMessageIdProvider diff --git a/package.json b/package.json index 0549681fe..712dc0350 100644 --- a/package.json +++ b/package.json @@ -1,113 +1,113 @@ -{ - "name": "mqtt", - "description": "A library for the MQTT protocol", - "version": "4.2.8", - "contributors": [ - "Adam Rudd ", - "Matteo Collina (https://github.com/mcollina)", - "Siarhei Buntsevich (https://github.com/scarry1992)", - "Yoseph Maguire (https://github.com/YoDaMa)" - ], - "keywords": [ - "mqtt", - "publish/subscribe", - "publish", - "subscribe" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "git://github.com/mqttjs/MQTT.js.git" - }, - "main": "mqtt.js", - "types": "types/index.d.ts", - "scripts": { - "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", - "pretest": "standard | snazzy", - "tslint": "tslint types/**/*.d.ts", - "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", - "typescript-compile-execute": "node test/typescript/*.js", - "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", - "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", - "prepare": "npm run browser-build", - "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", - "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", - "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" - }, - "pre-commit": [ - "pretest", - "tslint" - ], - "bin": { - "mqtt_pub": "./bin/pub.js", - "mqtt_sub": "./bin/sub.js", - "mqtt": "./bin/mqtt.js" - }, - "files": [ - "dist/", - "CONTRIBUTING.md", - "doc", - "lib", - "bin", - "types", - "mqtt.js" - ], - "engines": { - "node": ">=10.0.0" - }, - "browser": { - "./mqtt.js": "./lib/connect/index.js", - "fs": false, - "tls": false, - "net": false - }, - "dependencies": { - "collections": "^5.1.12", - "commist": "^1.0.0", - "concat-stream": "^2.0.0", - "debug": "^4.1.1", - "duplexify": "^4.1.1", - "help-me": "^3.0.0", - "inherits": "^2.0.3", - "minimist": "^1.2.5", - "mqtt-packet": "^6.8.0", - "number-allocator": "^1.0.7", - "pump": "^3.0.0", - "readable-stream": "^3.6.0", - "rfdc": "^1.3.0", - "reinterval": "^1.1.0", - "split2": "^3.1.0", - "ws": "^7.5.0", - "xtend": "^4.0.2" - }, - "devDependencies": { - "@types/node": "^10.0.0", - "@types/ws": "^8.2.0", - "aedes": "^0.42.5", - "airtap": "^3.0.0", - "browserify": "^16.5.0", - "chai": "^4.2.0", - "codecov": "^3.0.4", - "end-of-stream": "^1.4.1", - "global": "^4.3.2", - "mkdirp": "^0.5.1", - "mocha": "^4.1.0", - "mqtt-connection": "^4.0.0", - "nyc": "^15.0.1", - "pre-commit": "^1.2.2", - "rimraf": "^3.0.2", - "should": "^13.2.1", - "sinon": "^9.0.0", - "snazzy": "^8.0.0", - "standard": "^11.0.1", - "tslint": "^5.11.0", - "tslint-config-standard": "^8.0.1", - "typescript": "^3.2.2", - "uglify-es": "^3.3.9" - }, - "standard": { - "env": [ - "mocha" - ] - } -} +{ + "name": "mqtt", + "description": "A library for the MQTT protocol", + "version": "4.2.8", + "contributors": [ + "Adam Rudd ", + "Matteo Collina (https://github.com/mcollina)", + "Siarhei Buntsevich (https://github.com/scarry1992)", + "Yoseph Maguire (https://github.com/YoDaMa)" + ], + "keywords": [ + "mqtt", + "publish/subscribe", + "publish", + "subscribe" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/mqttjs/MQTT.js.git" + }, + "main": "mqtt.js", + "types": "types/index.d.ts", + "scripts": { + "test": "node_modules/.bin/nyc --reporter=lcov --reporter=text ./node_modules/mocha/bin/_mocha", + "pretest": "standard | snazzy", + "tslint": "tslint types/**/*.d.ts", + "typescript-compile-test": "tsc -p test/typescript/tsconfig.json", + "typescript-compile-execute": "node test/typescript/*.js", + "typescript-test": "npm run typescript-compile-test && npm run typescript-compile-execute", + "browser-build": "rimraf dist/ && mkdirp dist/ && browserify mqtt.js --standalone mqtt > dist/mqtt.js && uglifyjs dist/mqtt.js --compress --mangle --output dist/mqtt.min.js", + "prepare": "npm run browser-build", + "browser-test": "airtap --server test/browser/server.js --local --open test/browser/test.js", + "sauce-test": "airtap --server test/browser/server.js -- test/browser/test.js", + "ci": "npm run tslint && npm run typescript-compile-test && npm run test && codecov" + }, + "pre-commit": [ + "pretest", + "tslint" + ], + "bin": { + "mqtt_pub": "./bin/pub.js", + "mqtt_sub": "./bin/sub.js", + "mqtt": "./bin/mqtt.js" + }, + "files": [ + "dist/", + "CONTRIBUTING.md", + "doc", + "lib", + "bin", + "types", + "mqtt.js" + ], + "engines": { + "node": ">=10.0.0" + }, + "browser": { + "./mqtt.js": "./lib/connect/index.js", + "fs": false, + "tls": false, + "net": false + }, + "dependencies": { + "collections": "^5.1.12", + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.7", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "reinterval": "^1.1.0", + "split2": "^3.1.0", + "ws": "^7.5.0", + "xtend": "^4.0.2" + }, + "devDependencies": { + "@types/node": "^10.0.0", + "@types/ws": "^8.2.0", + "aedes": "^0.42.5", + "airtap": "^3.0.0", + "browserify": "^16.5.0", + "chai": "^4.2.0", + "codecov": "^3.0.4", + "end-of-stream": "^1.4.1", + "global": "^4.3.2", + "mkdirp": "^0.5.1", + "mocha": "^4.1.0", + "mqtt-connection": "^4.0.0", + "nyc": "^15.0.1", + "pre-commit": "^1.2.2", + "rimraf": "^3.0.2", + "should": "^13.2.1", + "sinon": "^9.0.0", + "snazzy": "^8.0.0", + "standard": "^11.0.1", + "tslint": "^5.11.0", + "tslint-config-standard": "^8.0.1", + "typescript": "^3.2.2", + "uglify-es": "^3.3.9" + }, + "standard": { + "env": [ + "mocha" + ] + } +} diff --git a/test/abstract_client.js b/test/abstract_client.js index fc1f2096f..4c8b0fa77 100644 --- a/test/abstract_client.js +++ b/test/abstract_client.js @@ -1,3177 +1,3177 @@ -'use strict' - -/** - * Testing dependencies - */ -var should = require('chai').should -var sinon = require('sinon') -var mqtt = require('../') -var xtend = require('xtend') -var Store = require('./../lib/store') -var assert = require('chai').assert -var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder - -module.exports = function (server, config) { - var version = config.protocolVersion || 4 - - function connect (opts) { - opts = xtend(config, opts) - return mqtt.connect(opts) - } - - describe('closing', function () { - it('should emit close if stream closes', function (done) { - var client = connect() - - client.once('connect', function () { - client.stream.end() - }) - client.once('close', function () { - client.end() - done() - }) - }) - - it('should mark the client as disconnected', function (done) { - var client = connect() - - client.once('close', function () { - client.end() - if (!client.connected) { - done() - } else { - done(new Error('Not marked as disconnected')) - } - }) - client.once('connect', function () { - client.stream.end() - }) - }) - - it('should stop ping timer if stream closes', function (done) { - var client = connect() - - client.once('close', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.stream.end() - }) - }) - - it('should emit close after end called', function (done) { - var client = connect() - - client.once('close', function () { - done() - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should emit end after end called and client must be disconnected', function (done) { - var client = connect() - - client.once('end', function () { - if (client.disconnected) { - return done() - } - done(new Error('client must be disconnected')) - }) - - client.once('connect', function () { - client.end() - }) - }) - - it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { - var store = new Store() - var client = connect({ incomingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { - var store = new Store() - var client = connect({ outgoingStore: store }) - - store.close = function (cb) { - cb(new Error('test')) - } - client.once('end', function () { - if (arguments.length === 0) { - return - } - throw new Error('no argument should be passed to event') - }) - - client.once('connect', function () { - client.end(function (testError) { - if (testError && testError.message === 'test') { - return done() - } - throw new Error('bad argument passed to callback') - }) - }) - }) - - it('should return `this` if end called twice', function (done) { - var client = connect() - - client.once('connect', function () { - client.end() - var value = client.end() - if (value === client) { - done() - } else { - done(new Error('Not returning client.')) - } - }) - }) - - it('should emit end only on first client end', function (done) { - var client = connect() - - client.once('end', function () { - var timeout = setTimeout(done.bind(null), 200) - client.once('end', function () { - clearTimeout(timeout) - done(new Error('end was emitted twice')) - }) - client.end() - }) - - client.once('connect', client.end.bind(client)) - }) - - it('should stop ping timer after end called', function (done) { - var client = connect() - - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(() => { - assert.notExists(client.pingTimer) - done() - }) - }) - }) - - it('should be able to end even on a failed connection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist'}) - - var timeout = setTimeout(function () { - done(new Error('Failed to end a disconnected client')) - }, 500) - - setTimeout(function () { - client.end(function () { - clearTimeout(timeout) - done() - }) - }, 200) - }) - - it('should emit end even on a failed connection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist'}) - - var timeout = setTimeout(function () { - done(new Error('Disconnected client has failed to emit end')) - }, 500) - - client.once('end', function () { - clearTimeout(timeout) - done() - }) - - // after 200ms manually invoke client.end - setTimeout(() => { - var boundEnd = client.end.bind(client) - boundEnd() - }, 200) - }) - - it.skip('should emit end only once for a reconnecting client', function (done) { - // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. - // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code - // there will be gists showing the difference between a successful test here and a failed test. For now we - // will add the retries syntax because of the flakiness. - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) - setTimeout(done.bind(null), 1000) - var endCallback = function () { - assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') - } - - var spy = sinon.spy(endCallback) - client.on('end', spy) - setTimeout(() => { - client.end.bind(client) - client.end() - }, 300) - }) - }) - - describe('connecting', function () { - it('should connect to the broker', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function () { - done() - client.end() - }) - }) - - it('should send a default client id', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'mqttjs') - client.end(done) - serverClient.disconnect() - }) - }) - }) - - it('should send be clean by default', function (done) { - var client = connect() - client.on('error', done) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.strictEqual(packet.clean, true) - serverClient.disconnect() - done() - }) - }) - }) - - it('should connect with the given client id', function (done) { - var client = connect({clientId: 'testclient'}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - serverClient.disconnect() - client.end(function (err) { - done(err) - }) - }) - }) - }) - - it('should connect with the client id and unclean state', function (done) { - var client = connect({clientId: 'testclient', clean: false}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - assert.isFalse(packet.clean) - client.end(false, function (err) { - serverClient.disconnect() - done(err) - }) - }) - }) - }) - - it('should require a clientId with clean=false', function (done) { - try { - var client = connect({ clean: false }) - client.on('error', function (err) { - done(err) - }) - } catch (err) { - assert.strictEqual(err.message, 'Missing clientId for unclean clients') - done() - } - }) - - it('should default to localhost', function (done) { - var client = connect({clientId: 'testclient'}) - client.on('error', function (err) { - throw err - }) - - server.once('client', function (serverClient) { - serverClient.once('connect', function (packet) { - assert.include(packet.clientId, 'testclient') - serverClient.disconnect() - done() - }) - }) - }) - - it('should emit connect', function (done) { - var client = connect() - client.once('connect', function () { - client.end(true, done) - }) - client.once('error', done) - }) - - it('should provide connack packet with connect event', function (done) { - var connack = version === 5 ? {reasonCode: 0} : {returnCode: 0} - server.once('client', function (serverClient) { - connack.sessionPresent = true - serverClient.connack(connack) - server.once('client', function (serverClient) { - connack.sessionPresent = false - serverClient.connack(connack) - }) - }) - - var client = connect() - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, true) - client.once('connect', function (packet) { - assert.strictEqual(packet.sessionPresent, false) - client.end() - done() - }) - }) - }) - - it('should mark the client as connected', function (done) { - var client = connect() - client.once('connect', function () { - client.end() - if (client.connected) { - done() - } else { - done(new Error('Not marked as connected')) - } - }) - }) - - it('should emit error on invalid clientId', function (done) { - var client = connect({clientId: 'invalid'}) - client.once('connect', function () { - done(new Error('Should not emit connect')) - }) - client.once('error', function (error) { - var value = version === 5 ? 128 : 2 - assert.strictEqual(error.code, value) // code for clientID identifer rejected - client.end() - done() - }) - }) - - it('should emit error event if the socket refuses the connection', function (done) { - // fake a port - var client = connect({ port: 4557 }) - - client.on('error', function (e) { - assert.equal(e.code, 'ECONNREFUSED') - client.end() - done() - }) - }) - - it('should have different client ids', function (done) { - // bug identified in this test: the client.end callback is invoked twice, once when the `end` - // method completes closing the stores and invokes the callback, and another time when the - // stream is closed. When the stream is closed, for some reason the closeStores method is called - // a second time. - var client1 = connect() - var client2 = connect() - - assert.notStrictEqual(client1.options.clientId, client2.options.clientId) - client1.end(true, () => { - client2.end(true, () => { - done() - }) - }) - }) - }) - - describe('handling offline states', function () { - it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { - var client = connect({reconnectPeriod: 20}) - - client.on('connect', function () { - this.stream.end() - }) - - client.on('offline', function () { - client.end(true, done) - }) - }) - - it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { - // fake a port - var client = connect({ reconnectPeriod: 20, port: 4557 }) - - client.on('error', function () {}) - - client.on('offline', function () { - client.end(true, done) - }) - }) - }) - - describe('topic validations when subscribing', function () { - it('should be ok for well-formated topics', function (done) { - var client = connect() - client.subscribe( - [ - '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', - 'system/+/event', 'system/registry/event/#', 'system/+/event/#', - 'system/registry/event/new_device', 'system/+/+/new_device' - ], - function (err) { - client.end(function () { - if (err) { - return done(new Error(err)) - } - done() - }) - } - ) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - var client = connect() - client.subscribe(['#/event', 'event#', 'event+'], function (err) { - client.end(false, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an empty array for duplicate subs', function (done) { - var client = connect() - client.subscribe('event', function (err, granted1) { - if (err) { - return done(err) - } - client.subscribe('event', function (err, granted2) { - if (err) { - return done(err) - } - assert.isArray(granted2) - assert.isEmpty(granted2) - done() - }) - }) - }) - - it('should return an error (via callbacks) for topic #/event', function (done) { - var client = connect() - client.subscribe('#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic event#', function (done) { - var client = connect() - client.subscribe('event#', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for topic system/#/event', function (done) { - var client = connect() - client.subscribe('system/#/event', function (err) { - client.end(function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - - it('should return an error (via callbacks) for empty topic list', function (done) { - var client = connect() - client.subscribe([], function (err) { - client.end() - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - - it('should return an error (via callbacks) for topic system/+/#/event', function (done) { - var client = connect() - client.subscribe('system/+/#/event', function (err) { - client.end(true, function () { - if (err) { - return done() - } - done(new Error('Validations do NOT work')) - }) - }) - }) - }) - - describe('offline messages', function () { - it('should queue message until connected', function (done) { - var client = connect() - - client.publish('test', 'test') - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 3) - - client.once('connect', function () { - assert.strictEqual(client.queue.length, 0) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not queue qos 0 messages if queueQoSZero is false', function (done) { - var client = connect({queueQoSZero: false}) - - client.publish('test', 'test', {qos: 0}) - assert.strictEqual(client.queue.length, 0) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should queue qos != 0 messages', function (done) { - var client = connect({queueQoSZero: false}) - - client.publish('test', 'test', {qos: 1}) - client.publish('test', 'test', {qos: 2}) - client.subscribe('test') - client.unsubscribe('test') - assert.strictEqual(client.queue.length, 2) - client.on('connect', function () { - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should not interrupt messages', function (done) { - var client = null - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var publishCount = 0 - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function () { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (packet.qos !== 0) { - serverClient.puback({messageId: packet.messageId}) - } - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - break - case 3: - assert.strictEqual(packet.payload.toString(), 'payload4') - server2.close() - done() - break - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore, - queueQoSZero: true - }) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'connack') { - setImmediate( - function () { - client.publish('test', 'payload3', {qos: 1}) - client.publish('test', 'payload4', {qos: 0}) - } - ) - } - }) - client.publish('test', 'payload1', {qos: 2}) - client.publish('test', 'payload2', {qos: 2}) - }) - }) - - it('should call cb if an outgoing QoS 0 message is not sent', function (done) { - var client = connect({queueQoSZero: false}) - var called = false - - client.publish('test', 'test', {qos: 0}, function () { - called = true - }) - - client.on('connect', function () { - assert.isTrue(called) - setTimeout(function () { - client.end(true, done) - }, 10) - }) - }) - - it('should delay ending up until all inflight messages are delivered', function (done) { - var client = connect() - var subscribeCalled = false - - client.on('connect', function () { - client.subscribe('test', function () { - subscribeCalled = true - }) - client.publish('test', 'test', function () { - client.end(false, function () { - assert.strictEqual(subscribeCalled, true) - done() - }) - }) - }) - }) - - it('wait QoS 1 publish messages', function (done) { - var client = connect() - var messageReceived = false - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 1 }, function () { - client.end(false, function () { - assert.strictEqual(messageReceived, true) - done() - }) - }) - client.on('message', function () { - messageReceived = true - }) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - }) - }) - - it('does not wait acks when force-closing', function (done) { - // non-running broker - var client = connect('mqtt://localhost:8993') - client.publish('test', 'test', { qos: 1 }) - client.end(true, done) - }) - - it('should call cb if store.put fails', function (done) { - const store = new Store() - store.put = function (packet, cb) { - process.nextTick(cb, new Error('oops there is an error')) - } - var client = connect({ incomingStore: store, outgoingStore: store }) - client.publish('test', 'test', { qos: 2 }, function (err) { - if (err) { - client.end(true, done) - } - }) - }) - }) - - describe('publishing', function () { - it('should publish a message (offline)', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - // don't wait on connect to send publish - client.publish(topic, payload) - - server.on('client', onClient) - - function onClient (serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (online)', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - // block on connect before sending publish - client.on('connect', function () { - client.publish(topic, payload) - }) - - server.on('client', onClient) - - function onClient (serverClient) { - serverClient.once('connect', function () { - server.removeListener('client', onClient) - }) - - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - }) - } - }) - - it('should publish a message (retain, offline)', function (done) { - var client = connect({ queueQoSZero: true }) - var payload = 'test' - var topic = 'test' - var called = false - - client.publish(topic, payload, { retain: true }, function () { - called = true - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, true) - assert.strictEqual(called, true) - client.end(true, done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var payload = 'test_payload' - var topic = 'testTopic' - - client.on('packetsend', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, 0) - assert.strictEqual(packet.retain, false) - client.end(true, done) - } else { - done(new Error('packet.cmd was not publish!')) - } - }) - - client.publish(topic, payload) - }) - - it('should accept options', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - var opts = { - retain: true, - qos: 1 - } - - client.once('connect', function () { - client.publish(topic, payload, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, false, 'incorrect dup') - client.end(done) - }) - }) - }) - - it('should publish with the default options for an empty parameter', function (done) { - var client = connect() - var payload = 'test' - var topic = 'test' - var defaultOpts = {qos: 0, retain: false, dup: false} - - client.once('connect', function () { - client.publish(topic, payload, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') - client.end(true, done) - }) - }) - }) - - it('should mark a message as duplicate when "dup" option is set', function (done) { - var client = connect() - var payload = 'duplicated-test' - var topic = 'test' - var opts = { - retain: true, - qos: 1, - dup: true - } - - client.once('connect', function () { - client.publish(topic, payload, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('publish', function (packet) { - assert.strictEqual(packet.topic, topic) - assert.strictEqual(packet.payload.toString(), payload) - assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') - assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') - assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') - client.end(done) - }) - }) - }) - - it('should fire a callback (qos 0)', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('a', 'b', function () { - client.end() - done() - }) - }) - }) - - it('should fire a callback (qos 1)', function (done) { - var client = connect() - var opts = { qos: 1 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end() - done() - }) - }) - }) - - it('should fire a callback (qos 2)', function (done) { - var client = connect() - var opts = { qos: 2 } - - client.once('connect', function () { - client.publish('a', 'b', opts, function () { - client.end() - done() - }) - }) - }) - - it('should support UTF-8 characters in topic', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('中国', 'hello', function () { - client.end() - done() - }) - }) - }) - - it('should support UTF-8 characters in payload', function (done) { - var client = connect() - - client.once('connect', function () { - client.publish('hello', '中国', function () { - client.end() - done() - }) - }) - }) - - it('should publish 10 QoS 2 and receive them', function (done) { - var client = connect() - var count = 0 - - client.on('connect', function () { - client.subscribe('test') - client.publish('test', 'test', { qos: 2 }) - }) - - client.on('message', function () { - if (count >= 10) { - client.end() - done() - } else { - client.publish('test', 'test', { qos: 2 }) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end() - done('error went offline... didnt see this happen') - }) - - serverClient.on('subscribe', function () { - serverClient.on('publish', function (packet) { - serverClient.publish(packet) - }) - }) - - serverClient.on('pubrel', function () { - count++ - }) - }) - }) - - function testQosHandleMessage (qos, done) { - var client = connect() - - var messageEventCount = 0 - var handleMessageCount = 0 - - client.handleMessage = function (packet, callback) { - setTimeout(function () { - handleMessageCount++ - // next message event should not emit until handleMessage completes - assert.strictEqual(handleMessageCount, messageEventCount) - if (handleMessageCount === 10) { - setTimeout(function () { - client.end(true, done) - }) - } - callback() - }, 100) - } - - client.on('message', function (topic, message, packet) { - messageEventCount++ - }) - - client.on('connect', function () { - client.subscribe('test') - }) - - server.once('client', function (serverClient) { - serverClient.on('offline', function () { - client.end(true, function () { - done('error went offline... didnt see this happen') - }) - }) - - serverClient.on('subscribe', function () { - for (var i = 0; i < 10; i++) { - serverClient.publish({ - messageId: i, - topic: 'test', - payload: 'test' + i, - qos: qos - }) - } - }) - }) - } - - var qosTests = [ 0, 1, 2 ] - qosTests.forEach(function (QoS) { - it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { - testQosHandleMessage(QoS, done) - }) - }) - - it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { - var client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client._sendPacket = sinon.spy() - - client._handlePublish({ - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }, function (err) { - assert.exists(err) - }) - - assert.strictEqual(client._sendPacket.callCount, 0) - client.end() - client.on('connect', function () { done() }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePublish` method', function (done) { - var client = connect() - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - try { - client._handlePublish({ - messageId: Math.floor(65535 * Math.random()), - topic: 'test', - payload: 'test', - qos: 1 - }) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - - it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePublish({ - messageId: 1, - topic: 'test', - payload: 'test', - qos: 1 - }, function () { - client.end() - done() - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePublish({ - messageId: 1, - topic: 'test', - payload: 'test', - qos: 2 - }, function () { - client.end() - done() - }) - }) - - it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del (packet, cb) { - process.nextTick(function () { - cb(new Error('Error')) - }) - } - - get (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({ incomingStore: store }) - - client._handlePubrel({ - messageId: 1, - qos: 2 - }, function () { - client.end(true, done) - }) - }) - - it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { - var delComplete = false - class AsyncStore { - put (packet, cb) { - process.nextTick(function () { - cb(null, 'Error') - }) - } - - del (packet, cb) { - process.nextTick(function () { - delComplete = true - cb(null) - }) - } - - get (packet, cb) { - process.nextTick(function () { - cb(null, {cmd: 'publish'}) - }) - } - - close (cb) { - cb() - } - } - - var store = new AsyncStore() - var client = connect({incomingStore: store}) - - client._handlePubrel({ - messageId: 1, - qos: 2 - }, function () { - assert.isTrue(delComplete) - client.end(true, done) - }) - }) - - it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { - var store = new Store() - var client = connect({incomingStore: store}) - - var messageId = Math.floor(65535 * Math.random()) - var topic = 'testTopic' - var payload = 'testPayload' - var qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, {qos: 2}) - - store.put({ - messageId: messageId, - topic: topic, - payload: payload, - qos: qos, - cmd: 'publish' - }, function () { - // cleans up the client - client._sendPacket = sinon.spy() - client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { - assert.exists(err) - assert.strictEqual(client._sendPacket.callCount, 0) - client.end(true, done) - }) - }) - }) - }) - - it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + - 'into `handlePubrel` method', function (done) { - var store = new Store() - var client = connect({incomingStore: store}) - - var messageId = Math.floor(65535 * Math.random()) - var topic = 'test' - var payload = 'test' - var qos = 2 - - client.handleMessage = function (packet, callback) { - callback(new Error('Error thrown by the application')) - } - - client.once('connect', function () { - client.subscribe(topic, {qos: 2}) - - store.put({ - messageId: messageId, - topic: topic, - payload: payload, - qos: qos, - cmd: 'publish' - }, function () { - try { - client._handlePubrel({cmd: 'pubrel', messageId: messageId}) - client.end(true, done) - } catch (err) { - client.end(true, () => { done(err) }) - } - }) - }) - }) - - it('should keep message order', function (done) { - var publishCount = 0 - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () {}) - - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({messageId: packet.messageId}) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - server2.close() - done() - break - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', {qos: 1}) - client.publish('topic', 'payload2', {qos: 1}) - client.end(true) - } else { - client.publish('topic', 'payload3', {qos: 1}) - } - }) - client.on('close', function () { - if (!reconnect) { - client.reconnect({ - clean: false, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - } - }) - }) - }) - - function testCallbackStorePutByQoS (qos, clean, expected, done) { - var client = connect({ - clean: clean, - clientId: 'testId' - }) - - var callbacks = [] - - function cbStorePut () { - callbacks.push('storeput') - } - - client.on('connect', function () { - client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { - if (err) done(err) - callbacks.push('publish') - assert.deepEqual(callbacks, expected) - client.end(true, done) - }) - }) - } - - var callbackStorePutByQoSParameters = [ - {args: [0, true], expected: ['publish']}, - {args: [0, false], expected: ['publish']}, - {args: [1, true], expected: ['storeput', 'publish']}, - {args: [1, false], expected: ['storeput', 'publish']}, - {args: [2, true], expected: ['storeput', 'publish']}, - {args: [2, false], expected: ['storeput', 'publish']} - ] - - callbackStorePutByQoSParameters.forEach(function (test) { - if (test.args[0] === 0) { // QoS 0 - it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } else { // QoS 1 and 2 - it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { - testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) - }) - } - }) - }) - - describe('unsubscribing', function () { - it('should send an unsubscribe packet (offline)', function (done) { - var client = connect() - - client.unsubscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, 'test') - client.end(done) - }) - }) - }) - - it('should send an unsubscribe packet', function (done) { - var client = connect() - var topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - client.end(done) - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - client.end(true, done) - } - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - client.end(true, done) - } - }) - }) - - it('should accept an array of unsubs', function (done) { - var client = connect() - var topics = ['topic1', 'topic2'] - - client.once('connect', function () { - client.unsubscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.deepStrictEqual(packet.unsubscriptions, topics) - client.end(done) - }) - }) - }) - - it('should fire a callback on unsuback', function (done) { - var client = connect() - var topic = 'topic' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - serverClient.unsuback(packet) - }) - }) - }) - - it('should unsubscribe from a chinese topic', function (done) { - var client = connect() - var topic = '中国' - - client.once('connect', function () { - client.unsubscribe(topic, () => { - client.end(err => { - done(err) - }) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('unsubscribe', function (packet) { - assert.include(packet.unsubscriptions, topic) - }) - }) - }) - }) - - describe('keepalive', function () { - var clock - - beforeEach(function () { - clock = sinon.useFakeTimers() - }) - - afterEach(function () { - clock.restore() - }) - - it('should checkPing at keepalive interval', function (done) { - var interval = 3 - var client = connect({ keepalive: interval }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 1) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 2) - - clock.tick(interval * 1000) - assert.strictEqual(client._checkPing.callCount, 3) - - client.end(true, done) - }) - }) - - it('should not checkPing if publishing at a higher rate than keepalive', function (done) { - var intervalMs = 3000 - var client = connect({keepalive: intervalMs / 1000}) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 0) - client.end(true, done) - }) - }) - - it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { - var intervalMs = 3000 - var client = connect({ - keepalive: intervalMs / 1000, - reschedulePings: false - }) - - client._checkPing = sinon.spy() - - client.once('connect', function () { - client.publish('foo', 'bar') - clock.tick(intervalMs - 1) - client.publish('foo', 'bar') - clock.tick(2) - - assert.strictEqual(client._checkPing.callCount, 1) - client.end(true, done) - }) - }) - }) - - describe('pinging', function () { - it('should set a ping timer', function (done) { - var client = connect({keepalive: 3}) - client.once('connect', function () { - assert.exists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should not set a ping timer keepalive=0', function (done) { - var client = connect({keepalive: 0}) - client.on('connect', function () { - assert.notExists(client.pingTimer) - client.end(true, done) - }) - }) - - it('should reconnect if pingresp is not sent', function (done) { - var client = connect({keepalive: 1, reconnectPeriod: 100}) - - // Fake no pingresp being send by stubbing the _handlePingresp function - client._handlePingresp = function () {} - - client.once('connect', function () { - client.once('connect', function () { - client.end(true, done) - }) - }) - }) - - it('should not reconnect if pingresp is successful', function (done) { - var client = connect({keepalive: 100}) - client.once('close', function () { - done(new Error('Client closed connection')) - }) - setTimeout(done, 1000) - }) - - it('should defer the next ping when sending a control packet', function (done) { - var client = connect({keepalive: 1}) - - client.once('connect', function () { - client._checkPing = sinon.spy() - - client.publish('foo', 'bar') - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - client.publish('foo', 'bar') - - setTimeout(function () { - assert.strictEqual(client._checkPing.callCount, 0) - done() - }, 75) - }, 75) - }, 75) - }) - }) - }) - - describe('subscribing', function () { - it('should send a subscribe message (offline)', function (done) { - var client = connect() - - client.subscribe('test') - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - done() - }) - }) - }) - - it('should send a subscribe message', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - done() - }) - }) - }) - - it('should emit a packetsend event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'subscribe') { - done() - } - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testTopic = 'testTopic' - - client.once('connect', function () { - client.subscribe(testTopic) - }) - - client.on('packetreceive', function (packet) { - if (packet.cmd === 'suback') { - done() - } - }) - }) - - it('should accept an array of subscriptions', function (done) { - var client = connect() - var subs = ['test1', 'test2'] - - client.once('connect', function () { - client.subscribe(subs) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] - var expected = subs.map(function (i) { - var result = {topic: i, qos: 0} - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - return result - }) - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept a hash of subscriptions', function (done) { - var client = connect() - var topics = { - test1: {qos: 0}, - test2: {qos: 1} - } - - client.once('connect', function () { - client.subscribe(topics) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var k - var expected = [] - - for (k in topics) { - if (topics.hasOwnProperty(k)) { - var result = { - topic: k, - qos: topics[k].qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - expected.push(result) - } - } - - assert.deepStrictEqual(packet.subscriptions, expected) - client.end(done) - }) - }) - }) - - it('should accept an options parameter', function (done) { - var client = connect() - var topic = 'test' - var opts = {qos: 1} - - client.once('connect', function () { - client.subscribe(topic, opts) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var expected = [{ - topic: topic, - qos: 1 - }] - - if (version === 5) { - expected[0].nl = false - expected[0].rap = false - expected[0].rh = 0 - } - - assert.deepStrictEqual(packet.subscriptions, expected) - done() - }) - }) - }) - - it('should subscribe with the default options for an empty options parameter', function (done) { - var client = connect() - var topic = 'test' - var defaultOpts = {qos: 0} - - client.once('connect', function () { - client.subscribe(topic, {}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: defaultOpts.qos - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - - assert.include(packet.subscriptions[0], result) - client.end(err => done(err)) - }) - }) - }) - - it('should fire a callback on suback', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.subscribe(topic, { qos: 2 }, function (err, granted) { - if (err) { - done(err) - } else { - assert.exists(granted, 'granted not given') - var expectedResult = {topic: 'test', qos: 2} - if (version === 5) { - expectedResult.nl = false - expectedResult.rap = false - expectedResult.rh = 0 - expectedResult.properties = undefined - } - assert.include(granted[0], expectedResult) - client.end(err => done(err)) - } - }) - }) - }) - - it('should fire a callback with error if disconnected (options provided)', function (done) { - var client = connect() - var topic = 'test' - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, {qos: 2}, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should fire a callback with error if disconnected (options not provided)', function (done) { - var client = connect() - var topic = 'test' - - client.once('connect', function () { - client.end(true, function () { - client.subscribe(topic, function (err, granted) { - assert.notExists(granted, 'granted given') - assert.exists(err, 'no error given') - done() - }) - }) - }) - }) - - it('should subscribe with a chinese topic', function (done) { - var client = connect() - var topic = '中国' - - client.once('connect', function () { - client.subscribe(topic) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function (packet) { - var result = { - topic: topic, - qos: 0 - } - if (version === 5) { - result.nl = false - result.rap = false - result.rh = 0 - } - assert.include(packet.subscriptions[0], result) - client.end(done) - }) - }) - }) - }) - - describe('receiving messages', function () { - it('should fire the message event', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - // - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a packetreceive event', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.on('packetreceive', function (packet) { - if (packet.cmd === 'publish') { - assert.strictEqual(packet.qos, 1) - assert.strictEqual(packet.topic, testPacket.topic) - assert.strictEqual(packet.payload.toString(), testPacket.payload) - assert.strictEqual(packet.retain, true) - client.end(true, done) - } - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should support binary data', function (done) { - var client = connect({ encoding: 'binary' }) - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.cmd, 'publish') - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2)', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - - it('should emit a message event (qos=2) - repeated publish', function (done) { - var client = connect() - var testPacket = { - topic: 'test', - payload: 'message', - retain: true, - qos: 2, - messageId: 5 - } - - server.testPublish = testPacket - - var messageHandler = function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - - assert.strictEqual(spiedMessageHandler.callCount, 1) - client.end(true, done) - } - - var spiedMessageHandler = sinon.spy(messageHandler) - - client.subscribe(testPacket.topic) - client.on('message', spiedMessageHandler) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - // twice, should be ignored - serverClient.publish(testPacket) - }) - }) - }) - - it('should support a chinese topic', function (done) { - var client = connect({ encoding: 'binary' }) - var testPacket = { - topic: '国', - payload: 'message', - retain: true, - qos: 1, - messageId: 5 - } - - client.subscribe(testPacket.topic) - client.once('message', function (topic, message, packet) { - assert.strictEqual(topic, testPacket.topic) - assert.instanceOf(message, Buffer) - assert.strictEqual(message.toString(), testPacket.payload) - assert.strictEqual(packet.messageId, testPacket.messageId) - assert.strictEqual(packet.qos, testPacket.qos) - client.end(true, done) - }) - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - serverClient.publish(testPacket) - }) - }) - }) - }) - - describe('qos handling', function () { - it('should follow qos 0 semantics (trivial)', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 0}, () => { - client.end(true, done) - }) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 0, - retain: false - }) - }) - }) - }) - - it('should follow qos 1 semantics', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 50 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 1}) - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - messageId: mid, - qos: 1 - }) - }) - - serverClient.once('puback', function (packet) { - assert.strictEqual(packet.messageId, mid) - client.end(done) - }) - }) - }) - - it('should follow qos 2 semantics', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - var publishReceived = 0 - var pubrecReceived = 0 - var pubrelReceived = 0 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - }) - - client.on('packetreceive', (packet) => { - switch (packet.cmd) { - case 'connack': - case 'suback': - // expected, but not specifically part of QOS 2 semantics - break - case 'publish': - assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') - assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') - publishReceived += 1 - break - case 'pubrel': - assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') - pubrelReceived += 1 - break - default: - should.fail() - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.on('pubrec', function () { - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') - assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') - pubrecReceived += 1 - }) - - serverClient.once('pubcomp', function () { - client.removeAllListeners() - serverClient.removeAllListeners() - assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') - assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') - assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') - client.end(true, done) - }) - }) - }) - - it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - }) - - client.on('packetreceive', (packet) => { - if (packet.cmd === 'pubrel') { - assert.strictEqual(client.incomingStore._inflights.size, 1) - } - }) - - server.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ - topic: testTopic, - payload: testMessage, - qos: 2, - messageId: mid - }) - }) - - serverClient.once('pubcomp', function () { - assert.strictEqual(client.incomingStore._inflights.size, 0) - client.removeAllListeners() - client.end(true, done) - }) - }) - }) - - function testMultiplePubrel (shouldSendPubcompFail, done) { - var client = connect() - var testTopic = 'test' - var testMessage = 'message' - var mid = 253 - var pubcompCount = 0 - var pubrelCount = 0 - var handleMessageCount = 0 - var emitMessageCount = 0 - var origSendPacket = client._sendPacket - var shouldSendFail - - client.handleMessage = function (packet, callback) { - handleMessageCount++ - callback() - } - - client.on('message', function () { - emitMessageCount++ - }) - - client._sendPacket = function (packet, sendDone) { - shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail - if (sendDone) { - sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) - } - - // send the mocked response - switch (packet.cmd) { - case 'subscribe': - const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} - client._handlePacket(suback, function (err) { - assert.isNotOk(err) - }) - break - case 'pubrec': - case 'pubcomp': - // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp - if (packet.cmd === 'pubcomp') { - pubcompCount++ - if (pubcompCount === 2) { - // end the test once the client has gone through two rounds of replying to pubrel messages - assert.strictEqual(pubrelCount, 2) - assert.strictEqual(handleMessageCount, 1) - assert.strictEqual(emitMessageCount, 1) - client._sendPacket = origSendPacket - client.end(true, done) - break - } - } - - // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received - const pubrel = {cmd: 'pubrel', messageId: mid} - pubrelCount++ - client._handlePacket(pubrel, function (err) { - if (shouldSendFail) { - assert.exists(err) - assert.instanceOf(err, Error) - } else { - assert.notExists(err) - } - }) - break - } - } - - client.once('connect', function () { - client.subscribe(testTopic, {qos: 2}) - const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} - client._handlePacket(publish, function (err) { - assert.notExists(err) - }) - }) - } - - it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { - testMultiplePubrel(false, done) - }) - - it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { - testMultiplePubrel(true, done) - }) - }) - - describe('auto reconnect', function () { - it('should mark the client disconnecting if #end called', function (done) { - var client = connect() - - client.end(true, err => { - assert.isTrue(client.disconnecting) - done(err) - }) - }) - - it('should reconnect after stream disconnect', function (done) { - var client = connect() - - var tryReconnect = true - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - client.end(true, done) - } - }) - }) - - it('should emit \'reconnect\' when reconnecting', function (done) { - var client = connect() - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - client.end(true, done) - } - }) - }) - - it('should emit \'offline\' after going offline', function (done) { - var client = connect() - - var tryReconnect = true - var offlineEvent = false - - client.on('offline', function () { - offlineEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.stream.end() - tryReconnect = false - } else { - assert.isTrue(offlineEvent) - client.end(true, done) - } - }) - }) - - it('should not reconnect if it was ended by the user', function (done) { - var client = connect() - - client.on('connect', function () { - client.end() - done() // it will raise an exception if called two times - }) - }) - - it('should setup a reconnect timer on disconnect', function (done) { - var client = connect() - - client.once('connect', function () { - assert.notExists(client.reconnectTimer) - client.stream.end() - }) - - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - - var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] - reconnectPeriodTests.forEach((test) => { - it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { - var end - var reconnectSlushTime = 200 - var client = connect({reconnectPeriod: test.period}) - var reconnect = false - var start = Date.now() - - client.on('connect', function () { - if (!reconnect) { - client.stream.end() - reconnect = true - } else { - end = Date.now() - client.end(() => { - let reconnectPeriodDuringTest = end - start - if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { - // give the connection a 200 ms slush window - done() - } else { - done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) - } - }) - } - }) - }) - }) - - it('should always cleanup successfully on reconnection', function (done) { - var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) - // bind client.end so that when it is called it is automatically passed in the done callback - setTimeout(client.end.bind(client, done), 50) - }) - - it('should resend in-flight QoS 1 publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - check() - }) - - function check () { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight publish messages if disconnecting', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - client.end(true, err => { - assert.isFalse(serverPublished) - assert.isFalse(clientCalledBack) - done(err) - }) - }) - }) - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - serverPublished = true - }) - }) - }) - client.publish('hello', 'world', { qos: 1 }, function () { - clientCalledBack = true - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var serverPublished = false - var clientCalledBack = false - - server.once('client', function (serverClient) { - // ignore errors - serverClient.on('error', function () {}) - serverClient.on('publish', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('pubrel', function () { - serverPublished = true - check() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function () { - clientCalledBack = true - check() - }) - - function check () { - if (serverPublished && clientCalledBack) { - client.end(true, done) - } - } - }) - - it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 1 }, function (err) { - clientCalledBack = true - assert.exists(err, 'error should exist') - assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, (err) => { - done(err) - }) - }) - - it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { - var client = connect({reconnectPeriod: 200}) - var clientCalledBack = false - - server.once('client', function (serverClient) { - serverClient.on('connect', function () { - setImmediate(function () { - serverClient.stream.destroy() - }) - }) - - server.once('client', function (serverClientNew) { - serverClientNew.on('publish', function () { - should.fail() - done() - }) - }) - }) - - client.publish('hello', 'world', { qos: 2 }, function (err) { - clientCalledBack = true - assert.strictEqual(err.message, 'Message removed') - }) - assert.strictEqual(Object.keys(client.outgoing).length, 1) - assert.strictEqual(client.outgoingStore._inflights.size, 1) - client.removeOutgoingMessage(client.getLastMessageId()) - assert.strictEqual(Object.keys(client.outgoing).length, 0) - assert.strictEqual(client.outgoingStore._inflights.size, 0) - assert.isTrue(clientCalledBack) - client.end(true, done) - }) - - it('should resubscribe when reconnecting', function (done) { - var client = connect({ reconnectPeriod: 100 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - client.end(done) - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { - var client = connect({ reconnectPeriod: 100, resubscribe: false }) - var tryReconnect = true - var reconnectEvent = false - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - client.end(true, done) - } - }) - }) - - it('should not resubscribe when reconnecting if suback is error', function (done) { - var tryReconnect = true - var reconnectEvent = false - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos | 0x80 - }) - }) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - var client = connect({ - port: ports.PORTAND49, - host: 'localhost', - reconnectPeriod: 100 - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - - server.once('client', function (serverClient) { - serverClient.on('subscribe', function () { - should.fail() - }) - }) - }) - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) - server2.close() - client.end(true, done) - } - }) - }) - }) - - it('should preserved incomingStore after disconnecting if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - if (reconnect) { - serverClient.pubrel({ messageId: 1 }) - } - }) - serverClient.on('subscribe', function (packet) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) - }) - serverClient.on('pubrec', function (packet) { - client.end(false, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - }) - }) - serverClient.on('pubcomp', function (packet) { - client.end(true, () => { - server2.close() - done() - }) - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.subscribe('test', {qos: 2}, function () { - }) - reconnect = true - } - }) - client.on('message', function (topic, message) { - assert.strictEqual(topic, 'topic') - assert.strictEqual(message.toString(), 'payload') - }) - }) - }) - - it('should clear outgoing if close from server', function (done) { - var reconnect = false - var client = {} - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('subscribe', function (packet) { - if (reconnect) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - serverClient.destroy() - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: true, - clientId: 'cid1', - keepalive: 1, - reconnectPeriod: 0 - }) - - client.on('connect', function () { - client.subscribe('test', {qos: 2}, function (e) { - if (!e) { - client.end() - } - }) - }) - - client.on('close', function () { - if (reconnect) { - server2.close() - done() - } else { - assert.strictEqual(Object.keys(client.outgoing).length, 0) - reconnect = true - client.reconnect() - } - }) - }) - }) - - it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, () => { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 1}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 2}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { - var reconnect = false - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - if (!reconnect) { - serverClient.pubrec({messageId: packet.messageId}) - } - }) - serverClient.on('pubrel', function () { - if (reconnect) { - server2.close() - client.end(true, done) - } else { - client.end(true, function () { - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - reconnect = true - }) - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload', {qos: 2}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should resend in-flight publish messages by published order', function (done) { - var publishCount = 0 - var reconnect = false - var disconnectOnce = true - var client = {} - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var server2 = serverBuilder(config.protocol, function (serverClient) { - // errors are not interesting for this test - // but they might happen on some platforms - serverClient.on('error', function () {}) - - serverClient.on('connect', function (packet) { - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - serverClient.connack(connack) - }) - serverClient.on('publish', function (packet) { - serverClient.puback({messageId: packet.messageId}) - if (reconnect) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.payload.toString(), 'payload1') - break - case 1: - assert.strictEqual(packet.payload.toString(), 'payload2') - break - case 2: - assert.strictEqual(packet.payload.toString(), 'payload3') - server2.close() - client.end(true, done) - break - } - } else { - if (disconnectOnce) { - client.end(true, function () { - reconnect = true - client.reconnect({ - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - }) - disconnectOnce = false - } - } - }) - }) - - server2.listen(ports.PORTAND50, function () { - client = connect({ - port: ports.PORTAND50, - host: 'localhost', - clean: false, - clientId: 'cid1', - reconnectPeriod: 0, - incomingStore: incomingStore, - outgoingStore: outgoingStore - }) - - client.nextId = 65535 - - client.on('connect', function () { - if (!reconnect) { - client.publish('topic', 'payload1', {qos: 1}) - client.publish('topic', 'payload2', {qos: 1}) - client.publish('topic', 'payload3', {qos: 1}) - } - }) - client.on('error', function () {}) - }) - }) - - it('should be able to pub/sub if reconnect() is called at close handler', function (done) { - var client = connect({ reconnectPeriod: 0 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - client.reconnect() - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { - var client = connect({ reconnectPeriod: 0 }) - var tryReconnect = true - var reconnectEvent = false - - client.on('close', function () { - if (tryReconnect) { - tryReconnect = false - setTimeout(function () { - client.reconnect() - }, 100) - } else { - assert.isTrue(reconnectEvent) - done() - } - }) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function () { - if (tryReconnect) { - client.end() - } else { - client.subscribe('hello', function () { - client.end() - }) - } - }) - }) - - context('with alternate server client', function () { - var cachedClientListeners - var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } - - beforeEach(function () { - cachedClientListeners = server.listeners('client') - server.removeAllListeners('client') - }) - - afterEach(function () { - server.removeAllListeners('client') - cachedClientListeners.forEach(function (listener) { - server.on('client', listener) - }) - }) - - it('should resubscribe even if disconnect is before suback', function (done) { - var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - var subscribeCount = 0 - var connectCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - connectCount++ - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, confirm that the only two - // subscribes have taken place, then cleanup and exit - if (connectCount >= 2) { - assert.strictEqual(subscribeCount, 2) - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - - it('should resubscribe exactly once', function (done) { - var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) - var subscribeCount = 0 - - server.on('client', function (serverClient) { - serverClient.on('connect', function () { - serverClient.connack(connack) - }) - - serverClient.on('subscribe', function () { - subscribeCount++ - - // disconnect before sending the suback on the first subscribe - if (subscribeCount === 1) { - client.stream.end() - } - - // after the second connection, only two subs - // subscribes have taken place, then cleanup and exit - if (subscribeCount === 2) { - client.end(true, done) - } - }) - }) - - client.subscribe('hello') - }) - }) - }) -} +'use strict' + +/** + * Testing dependencies + */ +var should = require('chai').should +var sinon = require('sinon') +var mqtt = require('../') +var xtend = require('xtend') +var Store = require('./../lib/store') +var assert = require('chai').assert +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder + +module.exports = function (server, config) { + var version = config.protocolVersion || 4 + + function connect (opts) { + opts = xtend(config, opts) + return mqtt.connect(opts) + } + + describe('closing', function () { + it('should emit close if stream closes', function (done) { + var client = connect() + + client.once('connect', function () { + client.stream.end() + }) + client.once('close', function () { + client.end() + done() + }) + }) + + it('should mark the client as disconnected', function (done) { + var client = connect() + + client.once('close', function () { + client.end() + if (!client.connected) { + done() + } else { + done(new Error('Not marked as disconnected')) + } + }) + client.once('connect', function () { + client.stream.end() + }) + }) + + it('should stop ping timer if stream closes', function (done) { + var client = connect() + + client.once('close', function () { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + + client.once('connect', function () { + assert.exists(client.pingTimer) + client.stream.end() + }) + }) + + it('should emit close after end called', function (done) { + var client = connect() + + client.once('close', function () { + done() + }) + + client.once('connect', function () { + client.end() + }) + }) + + it('should emit end after end called and client must be disconnected', function (done) { + var client = connect() + + client.once('end', function () { + if (client.disconnected) { + return done() + } + done(new Error('client must be disconnected')) + }) + + client.once('connect', function () { + client.end() + }) + }) + + it('should pass store close error to end callback but not to end listeners (incomingStore)', function (done) { + var store = new Store() + var client = connect({ incomingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should pass store close error to end callback but not to end listeners (outgoingStore)', function (done) { + var store = new Store() + var client = connect({ outgoingStore: store }) + + store.close = function (cb) { + cb(new Error('test')) + } + client.once('end', function () { + if (arguments.length === 0) { + return + } + throw new Error('no argument should be passed to event') + }) + + client.once('connect', function () { + client.end(function (testError) { + if (testError && testError.message === 'test') { + return done() + } + throw new Error('bad argument passed to callback') + }) + }) + }) + + it('should return `this` if end called twice', function (done) { + var client = connect() + + client.once('connect', function () { + client.end() + var value = client.end() + if (value === client) { + done() + } else { + done(new Error('Not returning client.')) + } + }) + }) + + it('should emit end only on first client end', function (done) { + var client = connect() + + client.once('end', function () { + var timeout = setTimeout(done.bind(null), 200) + client.once('end', function () { + clearTimeout(timeout) + done(new Error('end was emitted twice')) + }) + client.end() + }) + + client.once('connect', client.end.bind(client)) + }) + + it('should stop ping timer after end called', function (done) { + var client = connect() + + client.once('connect', function () { + assert.exists(client.pingTimer) + client.end(() => { + assert.notExists(client.pingTimer) + done() + }) + }) + }) + + it('should be able to end even on a failed connection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist'}) + + var timeout = setTimeout(function () { + done(new Error('Failed to end a disconnected client')) + }, 500) + + setTimeout(function () { + client.end(function () { + clearTimeout(timeout) + done() + }) + }, 200) + }) + + it('should emit end even on a failed connection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist'}) + + var timeout = setTimeout(function () { + done(new Error('Disconnected client has failed to emit end')) + }, 500) + + client.once('end', function () { + clearTimeout(timeout) + done() + }) + + // after 200ms manually invoke client.end + setTimeout(() => { + var boundEnd = client.end.bind(client) + boundEnd() + }, 200) + }) + + it.skip('should emit end only once for a reconnecting client', function (done) { + // I want to fix this test, but it will take signficant work, so I am marking it as a skipping test right now. + // Reason for it is that there are overlaps in the reconnectTimer and connectTimer. In the PR for this code + // there will be gists showing the difference between a successful test here and a failed test. For now we + // will add the retries syntax because of the flakiness. + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 10, reconnectPeriod: 20}) + setTimeout(done.bind(null), 1000) + var endCallback = function () { + assert.strictEqual(spy.callCount, 1, 'end was emitted more than once for reconnecting client') + } + + var spy = sinon.spy(endCallback) + client.on('end', spy) + setTimeout(() => { + client.end.bind(client) + client.end() + }, 300) + }) + }) + + describe('connecting', function () { + it('should connect to the broker', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function () { + done() + client.end() + }) + }) + + it('should send a default client id', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'mqttjs') + client.end(done) + serverClient.disconnect() + }) + }) + }) + + it('should send be clean by default', function (done) { + var client = connect() + client.on('error', done) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.strictEqual(packet.clean, true) + serverClient.disconnect() + done() + }) + }) + }) + + it('should connect with the given client id', function (done) { + var client = connect({clientId: 'testclient'}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + serverClient.disconnect() + client.end(function (err) { + done(err) + }) + }) + }) + }) + + it('should connect with the client id and unclean state', function (done) { + var client = connect({clientId: 'testclient', clean: false}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + assert.isFalse(packet.clean) + client.end(false, function (err) { + serverClient.disconnect() + done(err) + }) + }) + }) + }) + + it('should require a clientId with clean=false', function (done) { + try { + var client = connect({ clean: false }) + client.on('error', function (err) { + done(err) + }) + } catch (err) { + assert.strictEqual(err.message, 'Missing clientId for unclean clients') + done() + } + }) + + it('should default to localhost', function (done) { + var client = connect({clientId: 'testclient'}) + client.on('error', function (err) { + throw err + }) + + server.once('client', function (serverClient) { + serverClient.once('connect', function (packet) { + assert.include(packet.clientId, 'testclient') + serverClient.disconnect() + done() + }) + }) + }) + + it('should emit connect', function (done) { + var client = connect() + client.once('connect', function () { + client.end(true, done) + }) + client.once('error', done) + }) + + it('should provide connack packet with connect event', function (done) { + var connack = version === 5 ? {reasonCode: 0} : {returnCode: 0} + server.once('client', function (serverClient) { + connack.sessionPresent = true + serverClient.connack(connack) + server.once('client', function (serverClient) { + connack.sessionPresent = false + serverClient.connack(connack) + }) + }) + + var client = connect() + client.once('connect', function (packet) { + assert.strictEqual(packet.sessionPresent, true) + client.once('connect', function (packet) { + assert.strictEqual(packet.sessionPresent, false) + client.end() + done() + }) + }) + }) + + it('should mark the client as connected', function (done) { + var client = connect() + client.once('connect', function () { + client.end() + if (client.connected) { + done() + } else { + done(new Error('Not marked as connected')) + } + }) + }) + + it('should emit error on invalid clientId', function (done) { + var client = connect({clientId: 'invalid'}) + client.once('connect', function () { + done(new Error('Should not emit connect')) + }) + client.once('error', function (error) { + var value = version === 5 ? 128 : 2 + assert.strictEqual(error.code, value) // code for clientID identifer rejected + client.end() + done() + }) + }) + + it('should emit error event if the socket refuses the connection', function (done) { + // fake a port + var client = connect({ port: 4557 }) + + client.on('error', function (e) { + assert.equal(e.code, 'ECONNREFUSED') + client.end() + done() + }) + }) + + it('should have different client ids', function (done) { + // bug identified in this test: the client.end callback is invoked twice, once when the `end` + // method completes closing the stores and invokes the callback, and another time when the + // stream is closed. When the stream is closed, for some reason the closeStores method is called + // a second time. + var client1 = connect() + var client2 = connect() + + assert.notStrictEqual(client1.options.clientId, client2.options.clientId) + client1.end(true, () => { + client2.end(true, () => { + done() + }) + }) + }) + }) + + describe('handling offline states', function () { + it('should emit offline event once when the client transitions from connected states to disconnected ones', function (done) { + var client = connect({reconnectPeriod: 20}) + + client.on('connect', function () { + this.stream.end() + }) + + client.on('offline', function () { + client.end(true, done) + }) + }) + + it('should emit offline event once when the client (at first) can NOT connect to servers', function (done) { + // fake a port + var client = connect({ reconnectPeriod: 20, port: 4557 }) + + client.on('error', function () {}) + + client.on('offline', function () { + client.end(true, done) + }) + }) + }) + + describe('topic validations when subscribing', function () { + it('should be ok for well-formated topics', function (done) { + var client = connect() + client.subscribe( + [ + '+', '+/event', 'event/+', '#', 'event/#', 'system/event/+', + 'system/+/event', 'system/registry/event/#', 'system/+/event/#', + 'system/registry/event/new_device', 'system/+/+/new_device' + ], + function (err) { + client.end(function () { + if (err) { + return done(new Error(err)) + } + done() + }) + } + ) + }) + + it('should return an error (via callbacks) for topic #/event', function (done) { + var client = connect() + client.subscribe(['#/event', 'event#', 'event+'], function (err) { + client.end(false, function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an empty array for duplicate subs', function (done) { + var client = connect() + client.subscribe('event', function (err, granted1) { + if (err) { + return done(err) + } + client.subscribe('event', function (err, granted2) { + if (err) { + return done(err) + } + assert.isArray(granted2) + assert.isEmpty(granted2) + done() + }) + }) + }) + + it('should return an error (via callbacks) for topic #/event', function (done) { + var client = connect() + client.subscribe('#/event', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic event#', function (done) { + var client = connect() + client.subscribe('event#', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for topic system/#/event', function (done) { + var client = connect() + client.subscribe('system/#/event', function (err) { + client.end(function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + + it('should return an error (via callbacks) for empty topic list', function (done) { + var client = connect() + client.subscribe([], function (err) { + client.end() + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + + it('should return an error (via callbacks) for topic system/+/#/event', function (done) { + var client = connect() + client.subscribe('system/+/#/event', function (err) { + client.end(true, function () { + if (err) { + return done() + } + done(new Error('Validations do NOT work')) + }) + }) + }) + }) + + describe('offline messages', function () { + it('should queue message until connected', function (done) { + var client = connect() + + client.publish('test', 'test') + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 3) + + client.once('connect', function () { + assert.strictEqual(client.queue.length, 0) + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should not queue qos 0 messages if queueQoSZero is false', function (done) { + var client = connect({queueQoSZero: false}) + + client.publish('test', 'test', {qos: 0}) + assert.strictEqual(client.queue.length, 0) + client.on('connect', function () { + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should queue qos != 0 messages', function (done) { + var client = connect({queueQoSZero: false}) + + client.publish('test', 'test', {qos: 1}) + client.publish('test', 'test', {qos: 2}) + client.subscribe('test') + client.unsubscribe('test') + assert.strictEqual(client.queue.length, 2) + client.on('connect', function () { + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should not interrupt messages', function (done) { + var client = null + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var publishCount = 0 + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function () { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (packet.qos !== 0) { + serverClient.puback({messageId: packet.messageId}) + } + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + break + case 3: + assert.strictEqual(packet.payload.toString(), 'payload4') + server2.close() + done() + break + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore, + queueQoSZero: true + }) + client.on('packetreceive', function (packet) { + if (packet.cmd === 'connack') { + setImmediate( + function () { + client.publish('test', 'payload3', {qos: 1}) + client.publish('test', 'payload4', {qos: 0}) + } + ) + } + }) + client.publish('test', 'payload1', {qos: 2}) + client.publish('test', 'payload2', {qos: 2}) + }) + }) + + it('should call cb if an outgoing QoS 0 message is not sent', function (done) { + var client = connect({queueQoSZero: false}) + var called = false + + client.publish('test', 'test', {qos: 0}, function () { + called = true + }) + + client.on('connect', function () { + assert.isTrue(called) + setTimeout(function () { + client.end(true, done) + }, 10) + }) + }) + + it('should delay ending up until all inflight messages are delivered', function (done) { + var client = connect() + var subscribeCalled = false + + client.on('connect', function () { + client.subscribe('test', function () { + subscribeCalled = true + }) + client.publish('test', 'test', function () { + client.end(false, function () { + assert.strictEqual(subscribeCalled, true) + done() + }) + }) + }) + }) + + it('wait QoS 1 publish messages', function (done) { + var client = connect() + var messageReceived = false + + client.on('connect', function () { + client.subscribe('test') + client.publish('test', 'test', { qos: 1 }, function () { + client.end(false, function () { + assert.strictEqual(messageReceived, true) + done() + }) + }) + client.on('message', function () { + messageReceived = true + }) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.on('publish', function (packet) { + serverClient.publish(packet) + }) + }) + }) + }) + + it('does not wait acks when force-closing', function (done) { + // non-running broker + var client = connect('mqtt://localhost:8993') + client.publish('test', 'test', { qos: 1 }) + client.end(true, done) + }) + + it('should call cb if store.put fails', function (done) { + const store = new Store() + store.put = function (packet, cb) { + process.nextTick(cb, new Error('oops there is an error')) + } + var client = connect({ incomingStore: store, outgoingStore: store }) + client.publish('test', 'test', { qos: 2 }, function (err) { + if (err) { + client.end(true, done) + } + }) + }) + }) + + describe('publishing', function () { + it('should publish a message (offline)', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + // don't wait on connect to send publish + client.publish(topic, payload) + + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (online)', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + // block on connect before sending publish + client.on('connect', function () { + client.publish(topic, payload) + }) + + server.on('client', onClient) + + function onClient (serverClient) { + serverClient.once('connect', function () { + server.removeListener('client', onClient) + }) + + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + }) + } + }) + + it('should publish a message (retain, offline)', function (done) { + var client = connect({ queueQoSZero: true }) + var payload = 'test' + var topic = 'test' + var called = false + + client.publish(topic, payload, { retain: true }, function () { + called = true + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, true) + assert.strictEqual(called, true) + client.end(true, done) + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var payload = 'test_payload' + var topic = 'testTopic' + + client.on('packetsend', function (packet) { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, 0) + assert.strictEqual(packet.retain, false) + client.end(true, done) + } else { + done(new Error('packet.cmd was not publish!')) + } + }) + + client.publish(topic, payload) + }) + + it('should accept options', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + var opts = { + retain: true, + qos: 1 + } + + client.once('connect', function () { + client.publish(topic, payload, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, false, 'incorrect dup') + client.end(done) + }) + }) + }) + + it('should publish with the default options for an empty parameter', function (done) { + var client = connect() + var payload = 'test' + var topic = 'test' + var defaultOpts = {qos: 0, retain: false, dup: false} + + client.once('connect', function () { + client.publish(topic, payload, {}) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, defaultOpts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, defaultOpts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, defaultOpts.dup, 'incorrect dup') + client.end(true, done) + }) + }) + }) + + it('should mark a message as duplicate when "dup" option is set', function (done) { + var client = connect() + var payload = 'duplicated-test' + var topic = 'test' + var opts = { + retain: true, + qos: 1, + dup: true + } + + client.once('connect', function () { + client.publish(topic, payload, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('publish', function (packet) { + assert.strictEqual(packet.topic, topic) + assert.strictEqual(packet.payload.toString(), payload) + assert.strictEqual(packet.qos, opts.qos, 'incorrect qos') + assert.strictEqual(packet.retain, opts.retain, 'incorrect ret') + assert.strictEqual(packet.dup, opts.dup, 'incorrect dup') + client.end(done) + }) + }) + }) + + it('should fire a callback (qos 0)', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('a', 'b', function () { + client.end() + done() + }) + }) + }) + + it('should fire a callback (qos 1)', function (done) { + var client = connect() + var opts = { qos: 1 } + + client.once('connect', function () { + client.publish('a', 'b', opts, function () { + client.end() + done() + }) + }) + }) + + it('should fire a callback (qos 2)', function (done) { + var client = connect() + var opts = { qos: 2 } + + client.once('connect', function () { + client.publish('a', 'b', opts, function () { + client.end() + done() + }) + }) + }) + + it('should support UTF-8 characters in topic', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('中国', 'hello', function () { + client.end() + done() + }) + }) + }) + + it('should support UTF-8 characters in payload', function (done) { + var client = connect() + + client.once('connect', function () { + client.publish('hello', '中国', function () { + client.end() + done() + }) + }) + }) + + it('should publish 10 QoS 2 and receive them', function (done) { + var client = connect() + var count = 0 + + client.on('connect', function () { + client.subscribe('test') + client.publish('test', 'test', { qos: 2 }) + }) + + client.on('message', function () { + if (count >= 10) { + client.end() + done() + } else { + client.publish('test', 'test', { qos: 2 }) + } + }) + + server.once('client', function (serverClient) { + serverClient.on('offline', function () { + client.end() + done('error went offline... didnt see this happen') + }) + + serverClient.on('subscribe', function () { + serverClient.on('publish', function (packet) { + serverClient.publish(packet) + }) + }) + + serverClient.on('pubrel', function () { + count++ + }) + }) + }) + + function testQosHandleMessage (qos, done) { + var client = connect() + + var messageEventCount = 0 + var handleMessageCount = 0 + + client.handleMessage = function (packet, callback) { + setTimeout(function () { + handleMessageCount++ + // next message event should not emit until handleMessage completes + assert.strictEqual(handleMessageCount, messageEventCount) + if (handleMessageCount === 10) { + setTimeout(function () { + client.end(true, done) + }) + } + callback() + }, 100) + } + + client.on('message', function (topic, message, packet) { + messageEventCount++ + }) + + client.on('connect', function () { + client.subscribe('test') + }) + + server.once('client', function (serverClient) { + serverClient.on('offline', function () { + client.end(true, function () { + done('error went offline... didnt see this happen') + }) + }) + + serverClient.on('subscribe', function () { + for (var i = 0; i < 10; i++) { + serverClient.publish({ + messageId: i, + topic: 'test', + payload: 'test' + i, + qos: qos + }) + } + }) + }) + } + + var qosTests = [ 0, 1, 2 ] + qosTests.forEach(function (QoS) { + it('should publish 10 QoS ' + QoS + 'and receive them only when `handleMessage` finishes', function (done) { + testQosHandleMessage(QoS, done) + }) + }) + + it('should not send a `puback` if the execution of `handleMessage` fails for messages with QoS `1`', function (done) { + var client = connect() + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client._sendPacket = sinon.spy() + + client._handlePublish({ + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1 + }, function (err) { + assert.exists(err) + }) + + assert.strictEqual(client._sendPacket.callCount, 0) + client.end() + client.on('connect', function () { done() }) + }) + + it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePublish` method', function (done) { + var client = connect() + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + try { + client._handlePublish({ + messageId: Math.floor(65535 * Math.random()), + topic: 'test', + payload: 'test', + qos: 1 + }) + client.end(true, done) + } catch (err) { + client.end(true, () => { done(err) }) + } + }) + + it('should handle error with async incoming store in QoS 1 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePublish({ + messageId: 1, + topic: 'test', + payload: 'test', + qos: 1 + }, function () { + client.end() + done() + }) + }) + + it('should handle error with async incoming store in QoS 2 `handlePublish` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePublish({ + messageId: 1, + topic: 'test', + payload: 'test', + qos: 2 + }, function () { + client.end() + done() + }) + }) + + it('should handle error with async incoming store in QoS 2 `handlePubrel` method', function (done) { + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + cb(new Error('Error')) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({ incomingStore: store }) + + client._handlePubrel({ + messageId: 1, + qos: 2 + }, function () { + client.end(true, done) + }) + }) + + it('should handle success with async incoming store in QoS 2 `handlePubrel` method', function (done) { + var delComplete = false + class AsyncStore { + put (packet, cb) { + process.nextTick(function () { + cb(null, 'Error') + }) + } + + del (packet, cb) { + process.nextTick(function () { + delComplete = true + cb(null) + }) + } + + get (packet, cb) { + process.nextTick(function () { + cb(null, {cmd: 'publish'}) + }) + } + + close (cb) { + cb() + } + } + + var store = new AsyncStore() + var client = connect({incomingStore: store}) + + client._handlePubrel({ + messageId: 1, + qos: 2 + }, function () { + assert.isTrue(delComplete) + client.end(true, done) + }) + }) + + it('should not send a `pubcomp` if the execution of `handleMessage` fails for messages with QoS `2`', function (done) { + var store = new Store() + var client = connect({incomingStore: store}) + + var messageId = Math.floor(65535 * Math.random()) + var topic = 'testTopic' + var payload = 'testPayload' + var qos = 2 + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', function () { + client.subscribe(topic, {qos: 2}) + + store.put({ + messageId: messageId, + topic: topic, + payload: payload, + qos: qos, + cmd: 'publish' + }, function () { + // cleans up the client + client._sendPacket = sinon.spy() + client._handlePubrel({cmd: 'pubrel', messageId: messageId}, function (err) { + assert.exists(err) + assert.strictEqual(client._sendPacket.callCount, 0) + client.end(true, done) + }) + }) + }) + }) + + it('should silently ignore errors thrown by `handleMessage` and return when no callback is passed ' + + 'into `handlePubrel` method', function (done) { + var store = new Store() + var client = connect({incomingStore: store}) + + var messageId = Math.floor(65535 * Math.random()) + var topic = 'test' + var payload = 'test' + var qos = 2 + + client.handleMessage = function (packet, callback) { + callback(new Error('Error thrown by the application')) + } + + client.once('connect', function () { + client.subscribe(topic, {qos: 2}) + + store.put({ + messageId: messageId, + topic: topic, + payload: payload, + qos: qos, + cmd: 'publish' + }, function () { + try { + client._handlePubrel({cmd: 'pubrel', messageId: messageId}) + client.end(true, done) + } catch (err) { + client.end(true, () => { done(err) }) + } + }) + }) + }) + + it('should keep message order', function (done) { + var publishCount = 0 + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', function () {}) + + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + server2.close() + done() + break + } + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload1', {qos: 1}) + client.publish('topic', 'payload2', {qos: 1}) + client.end(true) + } else { + client.publish('topic', 'payload3', {qos: 1}) + } + }) + client.on('close', function () { + if (!reconnect) { + client.reconnect({ + clean: false, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + } + }) + }) + }) + + function testCallbackStorePutByQoS (qos, clean, expected, done) { + var client = connect({ + clean: clean, + clientId: 'testId' + }) + + var callbacks = [] + + function cbStorePut () { + callbacks.push('storeput') + } + + client.on('connect', function () { + client.publish('test', 'test', {qos: qos, cbStorePut: cbStorePut}, function (err) { + if (err) done(err) + callbacks.push('publish') + assert.deepEqual(callbacks, expected) + client.end(true, done) + }) + }) + } + + var callbackStorePutByQoSParameters = [ + {args: [0, true], expected: ['publish']}, + {args: [0, false], expected: ['publish']}, + {args: [1, true], expected: ['storeput', 'publish']}, + {args: [1, false], expected: ['storeput', 'publish']}, + {args: [2, true], expected: ['storeput', 'publish']}, + {args: [2, false], expected: ['storeput', 'publish']} + ] + + callbackStorePutByQoSParameters.forEach(function (test) { + if (test.args[0] === 0) { // QoS 0 + it('should not call cbStorePut when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } else { // QoS 1 and 2 + it('should call cbStorePut before publish completes when publishing message with QoS `' + test.args[0] + '` and clean `' + test.args[1] + '`', function (done) { + testCallbackStorePutByQoS(test.args[0], test.args[1], test.expected, done) + }) + } + }) + }) + + describe('unsubscribing', function () { + it('should send an unsubscribe packet (offline)', function (done) { + var client = connect() + + client.unsubscribe('test') + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, 'test') + client.end(done) + }) + }) + }) + + it('should send an unsubscribe packet', function (done) { + var client = connect() + var topic = 'topic' + + client.once('connect', function () { + client.unsubscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, topic) + client.end(done) + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'subscribe') { + client.end(true, done) + } + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetreceive', function (packet) { + if (packet.cmd === 'suback') { + client.end(true, done) + } + }) + }) + + it('should accept an array of unsubs', function (done) { + var client = connect() + var topics = ['topic1', 'topic2'] + + client.once('connect', function () { + client.unsubscribe(topics) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.deepStrictEqual(packet.unsubscriptions, topics) + client.end(done) + }) + }) + }) + + it('should fire a callback on unsuback', function (done) { + var client = connect() + var topic = 'topic' + + client.once('connect', function () { + client.unsubscribe(topic, () => { + client.end(true, done) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + serverClient.unsuback(packet) + }) + }) + }) + + it('should unsubscribe from a chinese topic', function (done) { + var client = connect() + var topic = '中国' + + client.once('connect', function () { + client.unsubscribe(topic, () => { + client.end(err => { + done(err) + }) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('unsubscribe', function (packet) { + assert.include(packet.unsubscriptions, topic) + }) + }) + }) + }) + + describe('keepalive', function () { + var clock + + beforeEach(function () { + clock = sinon.useFakeTimers() + }) + + afterEach(function () { + clock.restore() + }) + + it('should checkPing at keepalive interval', function (done) { + var interval = 3 + var client = connect({ keepalive: interval }) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 1) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 2) + + clock.tick(interval * 1000) + assert.strictEqual(client._checkPing.callCount, 3) + + client.end(true, done) + }) + }) + + it('should not checkPing if publishing at a higher rate than keepalive', function (done) { + var intervalMs = 3000 + var client = connect({keepalive: intervalMs / 1000}) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 0) + client.end(true, done) + }) + }) + + it('should checkPing if publishing at a higher rate than keepalive and reschedulePings===false', function (done) { + var intervalMs = 3000 + var client = connect({ + keepalive: intervalMs / 1000, + reschedulePings: false + }) + + client._checkPing = sinon.spy() + + client.once('connect', function () { + client.publish('foo', 'bar') + clock.tick(intervalMs - 1) + client.publish('foo', 'bar') + clock.tick(2) + + assert.strictEqual(client._checkPing.callCount, 1) + client.end(true, done) + }) + }) + }) + + describe('pinging', function () { + it('should set a ping timer', function (done) { + var client = connect({keepalive: 3}) + client.once('connect', function () { + assert.exists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should not set a ping timer keepalive=0', function (done) { + var client = connect({keepalive: 0}) + client.on('connect', function () { + assert.notExists(client.pingTimer) + client.end(true, done) + }) + }) + + it('should reconnect if pingresp is not sent', function (done) { + var client = connect({keepalive: 1, reconnectPeriod: 100}) + + // Fake no pingresp being send by stubbing the _handlePingresp function + client._handlePingresp = function () {} + + client.once('connect', function () { + client.once('connect', function () { + client.end(true, done) + }) + }) + }) + + it('should not reconnect if pingresp is successful', function (done) { + var client = connect({keepalive: 100}) + client.once('close', function () { + done(new Error('Client closed connection')) + }) + setTimeout(done, 1000) + }) + + it('should defer the next ping when sending a control packet', function (done) { + var client = connect({keepalive: 1}) + + client.once('connect', function () { + client._checkPing = sinon.spy() + + client.publish('foo', 'bar') + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + client.publish('foo', 'bar') + + setTimeout(function () { + assert.strictEqual(client._checkPing.callCount, 0) + done() + }, 75) + }, 75) + }, 75) + }) + }) + }) + + describe('subscribing', function () { + it('should send a subscribe message (offline)', function (done) { + var client = connect() + + client.subscribe('test') + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + done() + }) + }) + }) + + it('should send a subscribe message', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.subscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: 0 + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + done() + }) + }) + }) + + it('should emit a packetsend event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'subscribe') { + done() + } + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testTopic = 'testTopic' + + client.once('connect', function () { + client.subscribe(testTopic) + }) + + client.on('packetreceive', function (packet) { + if (packet.cmd === 'suback') { + done() + } + }) + }) + + it('should accept an array of subscriptions', function (done) { + var client = connect() + var subs = ['test1', 'test2'] + + client.once('connect', function () { + client.subscribe(subs) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + // i.e. [{topic: 'a', qos: 0}, {topic: 'b', qos: 0}] + var expected = subs.map(function (i) { + var result = {topic: i, qos: 0} + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + return result + }) + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept a hash of subscriptions', function (done) { + var client = connect() + var topics = { + test1: {qos: 0}, + test2: {qos: 1} + } + + client.once('connect', function () { + client.subscribe(topics) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var k + var expected = [] + + for (k in topics) { + if (topics.hasOwnProperty(k)) { + var result = { + topic: k, + qos: topics[k].qos + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + expected.push(result) + } + } + + assert.deepStrictEqual(packet.subscriptions, expected) + client.end(done) + }) + }) + }) + + it('should accept an options parameter', function (done) { + var client = connect() + var topic = 'test' + var opts = {qos: 1} + + client.once('connect', function () { + client.subscribe(topic, opts) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var expected = [{ + topic: topic, + qos: 1 + }] + + if (version === 5) { + expected[0].nl = false + expected[0].rap = false + expected[0].rh = 0 + } + + assert.deepStrictEqual(packet.subscriptions, expected) + done() + }) + }) + }) + + it('should subscribe with the default options for an empty options parameter', function (done) { + var client = connect() + var topic = 'test' + var defaultOpts = {qos: 0} + + client.once('connect', function () { + client.subscribe(topic, {}) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: defaultOpts.qos + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + + assert.include(packet.subscriptions[0], result) + client.end(err => done(err)) + }) + }) + }) + + it('should fire a callback on suback', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.subscribe(topic, { qos: 2 }, function (err, granted) { + if (err) { + done(err) + } else { + assert.exists(granted, 'granted not given') + var expectedResult = {topic: 'test', qos: 2} + if (version === 5) { + expectedResult.nl = false + expectedResult.rap = false + expectedResult.rh = 0 + expectedResult.properties = undefined + } + assert.include(granted[0], expectedResult) + client.end(err => done(err)) + } + }) + }) + }) + + it('should fire a callback with error if disconnected (options provided)', function (done) { + var client = connect() + var topic = 'test' + client.once('connect', function () { + client.end(true, function () { + client.subscribe(topic, {qos: 2}, function (err, granted) { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should fire a callback with error if disconnected (options not provided)', function (done) { + var client = connect() + var topic = 'test' + + client.once('connect', function () { + client.end(true, function () { + client.subscribe(topic, function (err, granted) { + assert.notExists(granted, 'granted given') + assert.exists(err, 'no error given') + done() + }) + }) + }) + }) + + it('should subscribe with a chinese topic', function (done) { + var client = connect() + var topic = '中国' + + client.once('connect', function () { + client.subscribe(topic) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function (packet) { + var result = { + topic: topic, + qos: 0 + } + if (version === 5) { + result.nl = false + result.rap = false + result.rh = 0 + } + assert.include(packet.subscriptions[0], result) + client.end(done) + }) + }) + }) + }) + + describe('receiving messages', function () { + it('should fire the message event', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + // + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a packetreceive event', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.on('packetreceive', function (packet) { + if (packet.cmd === 'publish') { + assert.strictEqual(packet.qos, 1) + assert.strictEqual(packet.topic, testPacket.topic) + assert.strictEqual(packet.payload.toString(), testPacket.payload) + assert.strictEqual(packet.retain, true) + client.end(true, done) + } + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should support binary data', function (done) { + var client = connect({ encoding: 'binary' }) + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.cmd, 'publish') + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2)', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5 + } + + server.testPublish = testPacket + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + + it('should emit a message event (qos=2) - repeated publish', function (done) { + var client = connect() + var testPacket = { + topic: 'test', + payload: 'message', + retain: true, + qos: 2, + messageId: 5 + } + + server.testPublish = testPacket + + var messageHandler = function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + + assert.strictEqual(spiedMessageHandler.callCount, 1) + client.end(true, done) + } + + var spiedMessageHandler = sinon.spy(messageHandler) + + client.subscribe(testPacket.topic) + client.on('message', spiedMessageHandler) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + // twice, should be ignored + serverClient.publish(testPacket) + }) + }) + }) + + it('should support a chinese topic', function (done) { + var client = connect({ encoding: 'binary' }) + var testPacket = { + topic: '国', + payload: 'message', + retain: true, + qos: 1, + messageId: 5 + } + + client.subscribe(testPacket.topic) + client.once('message', function (topic, message, packet) { + assert.strictEqual(topic, testPacket.topic) + assert.instanceOf(message, Buffer) + assert.strictEqual(message.toString(), testPacket.payload) + assert.strictEqual(packet.messageId, testPacket.messageId) + assert.strictEqual(packet.qos, testPacket.qos) + client.end(true, done) + }) + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + serverClient.publish(testPacket) + }) + }) + }) + }) + + describe('qos handling', function () { + it('should follow qos 0 semantics (trivial)', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 0}, () => { + client.end(true, done) + }) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 0, + retain: false + }) + }) + }) + }) + + it('should follow qos 1 semantics', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 50 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 1}) + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + messageId: mid, + qos: 1 + }) + }) + + serverClient.once('puback', function (packet) { + assert.strictEqual(packet.messageId, mid) + client.end(done) + }) + }) + }) + + it('should follow qos 2 semantics', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + var publishReceived = 0 + var pubrecReceived = 0 + var pubrelReceived = 0 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + }) + + client.on('packetreceive', (packet) => { + switch (packet.cmd) { + case 'connack': + case 'suback': + // expected, but not specifically part of QOS 2 semantics + break + case 'publish': + assert.strictEqual(pubrecReceived, 0, 'server received pubrec before client sent') + assert.strictEqual(pubrelReceived, 0, 'server received pubrec before client sent') + publishReceived += 1 + break + case 'pubrel': + assert.strictEqual(publishReceived, 1, 'only 1 publish must be received before a pubrel') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages (not only 1)') + pubrelReceived += 1 + break + default: + should.fail() + } + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid + }) + }) + + serverClient.on('pubrec', function () { + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages received') + assert.strictEqual(pubrecReceived, 0, 'invalid number of PUBREC messages recevied') + pubrecReceived += 1 + }) + + serverClient.once('pubcomp', function () { + client.removeAllListeners() + serverClient.removeAllListeners() + assert.strictEqual(publishReceived, 1, 'invalid number of PUBLISH messages') + assert.strictEqual(pubrecReceived, 1, 'invalid number of PUBREC messages') + assert.strictEqual(pubrelReceived, 1, 'invalid nubmer of PUBREL messages') + client.end(true, done) + }) + }) + }) + + it('should should empty the incoming store after a qos 2 handshake is completed', function (done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + }) + + client.on('packetreceive', (packet) => { + if (packet.cmd === 'pubrel') { + assert.strictEqual(client.incomingStore._inflights.size, 1) + } + }) + + server.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ + topic: testTopic, + payload: testMessage, + qos: 2, + messageId: mid + }) + }) + + serverClient.once('pubcomp', function () { + assert.strictEqual(client.incomingStore._inflights.size, 0) + client.removeAllListeners() + client.end(true, done) + }) + }) + }) + + function testMultiplePubrel (shouldSendPubcompFail, done) { + var client = connect() + var testTopic = 'test' + var testMessage = 'message' + var mid = 253 + var pubcompCount = 0 + var pubrelCount = 0 + var handleMessageCount = 0 + var emitMessageCount = 0 + var origSendPacket = client._sendPacket + var shouldSendFail + + client.handleMessage = function (packet, callback) { + handleMessageCount++ + callback() + } + + client.on('message', function () { + emitMessageCount++ + }) + + client._sendPacket = function (packet, sendDone) { + shouldSendFail = packet.cmd === 'pubcomp' && shouldSendPubcompFail + if (sendDone) { + sendDone(shouldSendFail ? new Error('testing pubcomp failure') : undefined) + } + + // send the mocked response + switch (packet.cmd) { + case 'subscribe': + const suback = {cmd: 'suback', messageId: packet.messageId, granted: [2]} + client._handlePacket(suback, function (err) { + assert.isNotOk(err) + }) + break + case 'pubrec': + case 'pubcomp': + // for both pubrec and pubcomp, reply with pubrel, simulating the server not receiving the pubcomp + if (packet.cmd === 'pubcomp') { + pubcompCount++ + if (pubcompCount === 2) { + // end the test once the client has gone through two rounds of replying to pubrel messages + assert.strictEqual(pubrelCount, 2) + assert.strictEqual(handleMessageCount, 1) + assert.strictEqual(emitMessageCount, 1) + client._sendPacket = origSendPacket + client.end(true, done) + break + } + } + + // simulate the pubrel message, either in response to pubrec or to mock pubcomp failing to be received + const pubrel = {cmd: 'pubrel', messageId: mid} + pubrelCount++ + client._handlePacket(pubrel, function (err) { + if (shouldSendFail) { + assert.exists(err) + assert.instanceOf(err, Error) + } else { + assert.notExists(err) + } + }) + break + } + } + + client.once('connect', function () { + client.subscribe(testTopic, {qos: 2}) + const publish = {cmd: 'publish', topic: testTopic, payload: testMessage, qos: 2, messageId: mid} + client._handlePacket(publish, function (err) { + assert.notExists(err) + }) + }) + } + + it('handle qos 2 messages exactly once when multiple pubrel received', function (done) { + testMultiplePubrel(false, done) + }) + + it('handle qos 2 messages exactly once when multiple pubrel received and sending pubcomp fails on client', function (done) { + testMultiplePubrel(true, done) + }) + }) + + describe('auto reconnect', function () { + it('should mark the client disconnecting if #end called', function (done) { + var client = connect() + + client.end(true, err => { + assert.isTrue(client.disconnecting) + done(err) + }) + }) + + it('should reconnect after stream disconnect', function (done) { + var client = connect() + + var tryReconnect = true + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + client.end(true, done) + } + }) + }) + + it('should emit \'reconnect\' when reconnecting', function (done) { + var client = connect() + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + client.end(true, done) + } + }) + }) + + it('should emit \'offline\' after going offline', function (done) { + var client = connect() + + var tryReconnect = true + var offlineEvent = false + + client.on('offline', function () { + offlineEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.stream.end() + tryReconnect = false + } else { + assert.isTrue(offlineEvent) + client.end(true, done) + } + }) + }) + + it('should not reconnect if it was ended by the user', function (done) { + var client = connect() + + client.on('connect', function () { + client.end() + done() // it will raise an exception if called two times + }) + }) + + it('should setup a reconnect timer on disconnect', function (done) { + var client = connect() + + client.once('connect', function () { + assert.notExists(client.reconnectTimer) + client.stream.end() + }) + + client.once('close', function () { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + + var reconnectPeriodTests = [ {period: 200}, {period: 2000}, {period: 4000} ] + reconnectPeriodTests.forEach((test) => { + it('should allow specification of a reconnect period (' + test.period + 'ms)', function (done) { + var end + var reconnectSlushTime = 200 + var client = connect({reconnectPeriod: test.period}) + var reconnect = false + var start = Date.now() + + client.on('connect', function () { + if (!reconnect) { + client.stream.end() + reconnect = true + } else { + end = Date.now() + client.end(() => { + let reconnectPeriodDuringTest = end - start + if (reconnectPeriodDuringTest >= test.period - reconnectSlushTime && reconnectPeriodDuringTest <= test.period + reconnectSlushTime) { + // give the connection a 200 ms slush window + done() + } else { + done(new Error('Strange reconnect period: ' + reconnectPeriodDuringTest)) + } + }) + } + }) + }) + }) + + it('should always cleanup successfully on reconnection', function (done) { + var client = connect({host: 'this_hostname_should_not_exist', connectTimeout: 0, reconnectPeriod: 1}) + // bind client.end so that when it is called it is automatically passed in the done callback + setTimeout(client.end.bind(client, done), 50) + }) + + it('should resend in-flight QoS 1 publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, function () { + clientCalledBack = true + check() + }) + + function check () { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight publish messages if disconnecting', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + client.end(true, err => { + assert.isFalse(serverPublished) + assert.isFalse(clientCalledBack) + done(err) + }) + }) + }) + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + serverPublished = true + }) + }) + }) + client.publish('hello', 'world', { qos: 1 }, function () { + clientCalledBack = true + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var serverPublished = false + var clientCalledBack = false + + server.once('client', function (serverClient) { + // ignore errors + serverClient.on('error', function () {}) + serverClient.on('publish', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('pubrel', function () { + serverPublished = true + check() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, function () { + clientCalledBack = true + check() + }) + + function check () { + if (serverPublished && clientCalledBack) { + client.end(true, done) + } + } + }) + + it('should not resend in-flight QoS 1 removed publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 1 }, function (err) { + clientCalledBack = true + assert.exists(err, 'error should exist') + assert.strictEqual(err.message, 'Message removed', 'error message is incorrect') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, (err) => { + done(err) + }) + }) + + it('should not resend in-flight QoS 2 removed publish messages from the client', function (done) { + var client = connect({reconnectPeriod: 200}) + var clientCalledBack = false + + server.once('client', function (serverClient) { + serverClient.on('connect', function () { + setImmediate(function () { + serverClient.stream.destroy() + }) + }) + + server.once('client', function (serverClientNew) { + serverClientNew.on('publish', function () { + should.fail() + done() + }) + }) + }) + + client.publish('hello', 'world', { qos: 2 }, function (err) { + clientCalledBack = true + assert.strictEqual(err.message, 'Message removed') + }) + assert.strictEqual(Object.keys(client.outgoing).length, 1) + assert.strictEqual(client.outgoingStore._inflights.size, 1) + client.removeOutgoingMessage(client.getLastMessageId()) + assert.strictEqual(Object.keys(client.outgoing).length, 0) + assert.strictEqual(client.outgoingStore._inflights.size, 0) + assert.isTrue(clientCalledBack) + client.end(true, done) + }) + + it('should resubscribe when reconnecting', function (done) { + var client = connect({ reconnectPeriod: 100 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + client.end(done) + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should not resubscribe when reconnecting if resubscribe is disabled', function (done) { + var client = connect({ reconnectPeriod: 100, resubscribe: false }) + var tryReconnect = true + var reconnectEvent = false + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + should.fail() + }) + }) + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + client.end(true, done) + } + }) + }) + + it('should not resubscribe when reconnecting if suback is error', function (done) { + var tryReconnect = true + var reconnectEvent = false + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos | 0x80 + }) + }) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + }) + }) + + server2.listen(ports.PORTAND49, function () { + var client = connect({ + port: ports.PORTAND49, + host: 'localhost', + reconnectPeriod: 100 + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + + server.once('client', function (serverClient) { + serverClient.on('subscribe', function () { + should.fail() + }) + }) + }) + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + assert.strictEqual(Object.keys(client._resubscribeTopics).length, 0) + server2.close() + client.end(true, done) + } + }) + }) + }) + + it('should preserved incomingStore after disconnecting if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + if (reconnect) { + serverClient.pubrel({ messageId: 1 }) + } + }) + serverClient.on('subscribe', function (packet) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + serverClient.publish({ topic: 'topic', payload: 'payload', qos: 2, messageId: 1, retain: false }) + }) + serverClient.on('pubrec', function (packet) { + client.end(false, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + }) + }) + serverClient.on('pubcomp', function (packet) { + client.end(true, () => { + server2.close() + done() + }) + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.subscribe('test', {qos: 2}, function () { + }) + reconnect = true + } + }) + client.on('message', function (topic, message) { + assert.strictEqual(topic, 'topic') + assert.strictEqual(message.toString(), 'payload') + }) + }) + }) + + it('should clear outgoing if close from server', function (done) { + var reconnect = false + var client = {} + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('subscribe', function (packet) { + if (reconnect) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + serverClient.destroy() + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: true, + clientId: 'cid1', + keepalive: 1, + reconnectPeriod: 0 + }) + + client.on('connect', function () { + client.subscribe('test', {qos: 2}, function (e) { + if (!e) { + client.end() + } + }) + }) + + client.on('close', function () { + if (reconnect) { + server2.close() + done() + } else { + assert.strictEqual(Object.keys(client.outgoing).length, 0) + reconnect = true + client.reconnect() + } + }) + }) + }) + + it('should resend in-flight QoS 1 publish messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, () => { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 1}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight QoS 2 publish messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 2}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight QoS 2 pubrel messages from the client if clean is false', function (done) { + var reconnect = false + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + if (!reconnect) { + serverClient.pubrec({messageId: packet.messageId}) + } + }) + serverClient.on('pubrel', function () { + if (reconnect) { + server2.close() + client.end(true, done) + } else { + client.end(true, function () { + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + reconnect = true + }) + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload', {qos: 2}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should resend in-flight publish messages by published order', function (done) { + var publishCount = 0 + var reconnect = false + var disconnectOnce = true + var client = {} + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var server2 = serverBuilder(config.protocol, function (serverClient) { + // errors are not interesting for this test + // but they might happen on some platforms + serverClient.on('error', function () {}) + + serverClient.on('connect', function (packet) { + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + serverClient.connack(connack) + }) + serverClient.on('publish', function (packet) { + serverClient.puback({messageId: packet.messageId}) + if (reconnect) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.payload.toString(), 'payload1') + break + case 1: + assert.strictEqual(packet.payload.toString(), 'payload2') + break + case 2: + assert.strictEqual(packet.payload.toString(), 'payload3') + server2.close() + client.end(true, done) + break + } + } else { + if (disconnectOnce) { + client.end(true, function () { + reconnect = true + client.reconnect({ + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + }) + disconnectOnce = false + } + } + }) + }) + + server2.listen(ports.PORTAND50, function () { + client = connect({ + port: ports.PORTAND50, + host: 'localhost', + clean: false, + clientId: 'cid1', + reconnectPeriod: 0, + incomingStore: incomingStore, + outgoingStore: outgoingStore + }) + + client.nextId = 65535 + + client.on('connect', function () { + if (!reconnect) { + client.publish('topic', 'payload1', {qos: 1}) + client.publish('topic', 'payload2', {qos: 1}) + client.publish('topic', 'payload3', {qos: 1}) + } + }) + client.on('error', function () {}) + }) + }) + + it('should be able to pub/sub if reconnect() is called at close handler', function (done) { + var client = connect({ reconnectPeriod: 0 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('close', function () { + if (tryReconnect) { + tryReconnect = false + client.reconnect() + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', function () { + client.end() + }) + } + }) + }) + + it('should be able to pub/sub if reconnect() is called at out of close handler', function (done) { + var client = connect({ reconnectPeriod: 0 }) + var tryReconnect = true + var reconnectEvent = false + + client.on('close', function () { + if (tryReconnect) { + tryReconnect = false + setTimeout(function () { + client.reconnect() + }, 100) + } else { + assert.isTrue(reconnectEvent) + done() + } + }) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function () { + if (tryReconnect) { + client.end() + } else { + client.subscribe('hello', function () { + client.end() + }) + } + }) + }) + + context('with alternate server client', function () { + var cachedClientListeners + var connack = version === 5 ? { reasonCode: 0 } : { returnCode: 0 } + + beforeEach(function () { + cachedClientListeners = server.listeners('client') + server.removeAllListeners('client') + }) + + afterEach(function () { + server.removeAllListeners('client') + cachedClientListeners.forEach(function (listener) { + server.on('client', listener) + }) + }) + + it('should resubscribe even if disconnect is before suback', function (done) { + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) + var subscribeCount = 0 + var connectCount = 0 + + server.on('client', function (serverClient) { + serverClient.on('connect', function () { + connectCount++ + serverClient.connack(connack) + }) + + serverClient.on('subscribe', function () { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, confirm that the only two + // subscribes have taken place, then cleanup and exit + if (connectCount >= 2) { + assert.strictEqual(subscribeCount, 2) + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + + it('should resubscribe exactly once', function (done) { + var client = connect(Object.assign({ reconnectPeriod: 100 }, config)) + var subscribeCount = 0 + + server.on('client', function (serverClient) { + serverClient.on('connect', function () { + serverClient.connack(connack) + }) + + serverClient.on('subscribe', function () { + subscribeCount++ + + // disconnect before sending the suback on the first subscribe + if (subscribeCount === 1) { + client.stream.end() + } + + // after the second connection, only two subs + // subscribes have taken place, then cleanup and exit + if (subscribeCount === 2) { + client.end(true, done) + } + }) + }) + + client.subscribe('hello') + }) + }) + }) +} diff --git a/test/abstract_store.js b/test/abstract_store.js index 33b78106d..02b3ec849 100644 --- a/test/abstract_store.js +++ b/test/abstract_store.js @@ -1,135 +1,135 @@ -'use strict' - -require('should') - -module.exports = function abstractStoreTest (build) { - var store - - beforeEach(function (done) { - build(function (err, _store) { - store = _store - done(err) - }) - }) - - afterEach(function (done) { - store.close(done) - }) - - it('should put and stream in-flight packets', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet) - done() - }) - }) - }) - - it('should support destroying the stream', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - var stream = store.createStream() - stream.on('close', done) - stream.destroy() - }) - }) - - it('should add and del in-flight packets', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del(packet, function () { - store - .createStream() - .on('data', function () { - done(new Error('this should never happen')) - }) - .on('end', done) - }) - }) - }) - - it('should replace a packet when doing put with the same messageId', function (done) { - var packet1 = { - cmd: 'publish', // added - topic: 'hello', - payload: 'world', - qos: 2, - messageId: 42 - } - var packet2 = { - cmd: 'pubrel', // added - qos: 2, - messageId: 42 - } - - store.put(packet1, function () { - store.put(packet2, function () { - store - .createStream() - .on('data', function (data) { - data.should.eql(packet2) - done() - }) - }) - }) - }) - - it('should return the original packet on del', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.del({ messageId: 42 }, function (err, deleted) { - if (err) { - throw err - } - deleted.should.eql(packet) - done() - }) - }) - }) - - it('should get a packet with the same messageId', function (done) { - var packet = { - topic: 'hello', - payload: 'world', - qos: 1, - messageId: 42 - } - - store.put(packet, function () { - store.get({ messageId: 42 }, function (err, fromDb) { - if (err) { - throw err - } - fromDb.should.eql(packet) - done() - }) - }) - }) -} +'use strict' + +require('should') + +module.exports = function abstractStoreTest (build) { + var store + + beforeEach(function (done) { + build(function (err, _store) { + store = _store + done(err) + }) + }) + + afterEach(function (done) { + store.close(done) + }) + + it('should put and stream in-flight packets', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store + .createStream() + .on('data', function (data) { + data.should.eql(packet) + done() + }) + }) + }) + + it('should support destroying the stream', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + var stream = store.createStream() + stream.on('close', done) + stream.destroy() + }) + }) + + it('should add and del in-flight packets', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.del(packet, function () { + store + .createStream() + .on('data', function () { + done(new Error('this should never happen')) + }) + .on('end', done) + }) + }) + }) + + it('should replace a packet when doing put with the same messageId', function (done) { + var packet1 = { + cmd: 'publish', // added + topic: 'hello', + payload: 'world', + qos: 2, + messageId: 42 + } + var packet2 = { + cmd: 'pubrel', // added + qos: 2, + messageId: 42 + } + + store.put(packet1, function () { + store.put(packet2, function () { + store + .createStream() + .on('data', function (data) { + data.should.eql(packet2) + done() + }) + }) + }) + }) + + it('should return the original packet on del', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.del({ messageId: 42 }, function (err, deleted) { + if (err) { + throw err + } + deleted.should.eql(packet) + done() + }) + }) + }) + + it('should get a packet with the same messageId', function (done) { + var packet = { + topic: 'hello', + payload: 'world', + qos: 1, + messageId: 42 + } + + store.put(packet, function () { + store.get({ messageId: 42 }, function (err, fromDb) { + if (err) { + throw err + } + fromDb.should.eql(packet) + done() + }) + }) + }) +} diff --git a/test/browser/server.js b/test/browser/server.js index c4cf66b96..75a9a8994 100644 --- a/test/browser/server.js +++ b/test/browser/server.js @@ -1,132 +1,132 @@ -'use strict' - -var handleClient -var WS = require('ws') -var WebSocketServer = WS.Server -var Connection = require('mqtt-connection') -var http = require('http') - -handleClient = function (client) { - var self = this - - if (!self.clients) { - self.clients = {} - } - - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) - } else { - client.connack({returnCode: 0}) - } - self.clients[packet.clientId] = client - client.subscriptions = [] - }) - - client.on('publish', function (packet) { - var i, k, c, s, publish - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - - for (k in self.clients) { - c = self.clients[k] - publish = false - - for (i = 0; i < c.subscriptions.length; i++) { - s = c.subscriptions[i] - - if (s.test(packet.topic)) { - publish = true - } - } - - if (publish) { - try { - c.publish({topic: packet.topic, payload: packet.payload}) - } catch (error) { - delete self.clients[k] - } - } - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - var qos - var topic - var reg - var granted = [] - - for (var i = 0; i < packet.subscriptions.length; i++) { - qos = packet.subscriptions[i].qos - topic = packet.subscriptions[i].topic - reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') - - granted.push(qos) - client.subscriptions.push(reg) - } - - client.suback({messageId: packet.messageId, granted: granted}) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -function start (startPort, done) { - var server = http.createServer() - var wss = new WebSocketServer({server: server}) - - wss.on('connection', function (ws) { - var stream, connection - - if (!(ws.protocol === 'mqtt' || - ws.protocol === 'mqttv3.1')) { - return ws.close() - } - - stream = WS.createWebSocketStream(ws) - connection = new Connection(stream) - handleClient.call(server, connection) - }) - server.listen(startPort, done) - server.on('request', function (req, res) { - res.statusCode = 404 - res.end('Not Found') - }) - return server -} - -if (require.main === module) { - start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { - if (err) { - console.error(err) - return - } - console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) - }) -} +'use strict' + +var handleClient +var WS = require('ws') +var WebSocketServer = WS.Server +var Connection = require('mqtt-connection') +var http = require('http') + +handleClient = function (client) { + var self = this + + if (!self.clients) { + self.clients = {} + } + + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({returnCode: 2}) + } else { + client.connack({returnCode: 0}) + } + self.clients[packet.clientId] = client + client.subscriptions = [] + }) + + client.on('publish', function (packet) { + var i, k, c, s, publish + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + + for (k in self.clients) { + c = self.clients[k] + publish = false + + for (i = 0; i < c.subscriptions.length; i++) { + s = c.subscriptions[i] + + if (s.test(packet.topic)) { + publish = true + } + } + + if (publish) { + try { + c.publish({topic: packet.topic, payload: packet.payload}) + } catch (error) { + delete self.clients[k] + } + } + } + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + var qos + var topic + var reg + var granted = [] + + for (var i = 0; i < packet.subscriptions.length; i++) { + qos = packet.subscriptions[i].qos + topic = packet.subscriptions[i].topic + reg = new RegExp(topic.replace('+', '[^/]+').replace('#', '.+') + '$') + + granted.push(qos) + client.subscriptions.push(reg) + } + + client.suback({messageId: packet.messageId, granted: granted}) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +function start (startPort, done) { + var server = http.createServer() + var wss = new WebSocketServer({server: server}) + + wss.on('connection', function (ws) { + var stream, connection + + if (!(ws.protocol === 'mqtt' || + ws.protocol === 'mqttv3.1')) { + return ws.close() + } + + stream = WS.createWebSocketStream(ws) + connection = new Connection(stream) + handleClient.call(server, connection) + }) + server.listen(startPort, done) + server.on('request', function (req, res) { + res.statusCode = 404 + res.end('Not Found') + }) + return server +} + +if (require.main === module) { + start(process.env.PORT || process.env.AIRTAP_PORT, function (err) { + if (err) { + console.error(err) + return + } + console.log('tunnelled server started on port', process.env.PORT || process.env.AIRTAP_PORT) + }) +} diff --git a/test/browser/test.js b/test/browser/test.js index 78fa93cc5..8e9cd42e3 100644 --- a/test/browser/test.js +++ b/test/browser/test.js @@ -1,92 +1,92 @@ -'use strict' - -var mqtt = require('../../lib/connect') -var xtend = require('xtend') -var _URL = require('url') -var parsed = _URL.parse(document.URL) -var isHttps = parsed.protocol === 'https:' -var port = parsed.port || (isHttps ? 443 : 80) -var host = parsed.hostname -var protocol = isHttps ? 'wss' : 'ws' - -function clientTests (buildClient) { - var client - - beforeEach(function () { - client = buildClient() - client.on('offline', function () { - console.log('client offline') - }) - client.on('connect', function () { - console.log('client connect') - }) - client.on('reconnect', function () { - console.log('client reconnect') - }) - }) - - afterEach(function (done) { - client.once('close', function () { - done() - }) - client.end() - }) - - it('should connect', function (done) { - client.on('connect', function () { - done() - }) - }) - - it('should publish and subscribe', function (done) { - client.subscribe('hello', function () { - done() - }).publish('hello', 'world') - }) -} - -function suiteFactory (configName, opts) { - function setVersion (base) { - return xtend(base || {}, opts) - } - - var suiteName = 'MqttClient(' + configName + '=' + JSON.stringify(opts) + ')' - describe(suiteName, function () { - this.timeout(10000) - - describe('specifying nothing', function () { - clientTests(function () { - return mqtt.connect(setVersion()) - }) - }) - - if (parsed.hostname === 'localhost') { - describe('specifying a port', function () { - clientTests(function () { - return mqtt.connect(setVersion({ protocol: protocol, port: port })) - }) - }) - } - - describe('specifying a port and host', function () { - clientTests(function () { - return mqtt.connect(setVersion({ protocol: protocol, port: port, host: host })) - }) - }) - - describe('specifying a URL', function () { - clientTests(function () { - return mqtt.connect(protocol + '://' + host + ':' + port, setVersion()) - }) - }) - - describe('specifying a URL with a path', function () { - clientTests(function () { - return mqtt.connect(protocol + '://' + host + ':' + port + '/mqtt', setVersion()) - }) - }) - }) -} - -suiteFactory('v3', {protocolId: 'MQIsdp', protocolVersion: 3}) -suiteFactory('default', {}) +'use strict' + +var mqtt = require('../../lib/connect') +var xtend = require('xtend') +var _URL = require('url') +var parsed = _URL.parse(document.URL) +var isHttps = parsed.protocol === 'https:' +var port = parsed.port || (isHttps ? 443 : 80) +var host = parsed.hostname +var protocol = isHttps ? 'wss' : 'ws' + +function clientTests (buildClient) { + var client + + beforeEach(function () { + client = buildClient() + client.on('offline', function () { + console.log('client offline') + }) + client.on('connect', function () { + console.log('client connect') + }) + client.on('reconnect', function () { + console.log('client reconnect') + }) + }) + + afterEach(function (done) { + client.once('close', function () { + done() + }) + client.end() + }) + + it('should connect', function (done) { + client.on('connect', function () { + done() + }) + }) + + it('should publish and subscribe', function (done) { + client.subscribe('hello', function () { + done() + }).publish('hello', 'world') + }) +} + +function suiteFactory (configName, opts) { + function setVersion (base) { + return xtend(base || {}, opts) + } + + var suiteName = 'MqttClient(' + configName + '=' + JSON.stringify(opts) + ')' + describe(suiteName, function () { + this.timeout(10000) + + describe('specifying nothing', function () { + clientTests(function () { + return mqtt.connect(setVersion()) + }) + }) + + if (parsed.hostname === 'localhost') { + describe('specifying a port', function () { + clientTests(function () { + return mqtt.connect(setVersion({ protocol: protocol, port: port })) + }) + }) + } + + describe('specifying a port and host', function () { + clientTests(function () { + return mqtt.connect(setVersion({ protocol: protocol, port: port, host: host })) + }) + }) + + describe('specifying a URL', function () { + clientTests(function () { + return mqtt.connect(protocol + '://' + host + ':' + port, setVersion()) + }) + }) + + describe('specifying a URL with a path', function () { + clientTests(function () { + return mqtt.connect(protocol + '://' + host + ':' + port + '/mqtt', setVersion()) + }) + }) + }) +} + +suiteFactory('v3', {protocolId: 'MQIsdp', protocolVersion: 3}) +suiteFactory('default', {}) diff --git a/test/client.js b/test/client.js index 0b3c4228a..4ea052ab8 100644 --- a/test/client.js +++ b/test/client.js @@ -1,486 +1,486 @@ -'use strict' - -var mqtt = require('..') -var assert = require('chai').assert -const { fork } = require('child_process') -var path = require('path') -var abstractClientTests = require('./abstract_client') -var net = require('net') -var eos = require('end-of-stream') -var mqttPacket = require('mqtt-packet') -var Duplex = require('readable-stream').Duplex -var Connection = require('mqtt-connection') -var MqttServer = require('./server').MqttServer -var util = require('util') -var ports = require('./helpers/port_list') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var debug = require('debug')('TEST:client') - -describe('MqttClient', function () { - var client - var server = serverBuilder('mqtt') - var config = {protocol: 'mqtt', port: ports.PORT} - server.listen(ports.PORT) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) - - describe('creating', function () { - it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { - try { - client = mqtt.MqttClient(function () { - throw Error('break') - }, {}) - client.end() - } catch (err) { - assert.strictEqual(err.message, 'break') - done() - } - }) - }) - - describe('message ids', function () { - it('should increment the message id', function () { - client = mqtt.connect(config) - var currentId = client._nextId() - - assert.equal(client._nextId(), currentId + 1) - client.end() - }) - - it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { - var server2 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) - }) - }) - - server2.listen(ports.PORTAND49, function () { - client = mqtt.connect({ - port: ports.PORTAND49, - host: 'localhost' - }) - - client.on('packetsend', function (packet) { - if (packet.cmd === 'pubcomp') { - client.end() - server2.close() - done() - } - }) - }) - }) - - it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { - var parser = mqttPacket.parser() - var count = 0 - var max = 1000 - var duplex = new Duplex({ - read: function (n) {}, - write: function (chunk, enc, cb) { - parser.parse(chunk) - cb() // nothing to do - } - }) - client = new mqtt.MqttClient(function () { - return duplex - }, {}) - - client.on('message', function (t, p, packet) { - if (++count === max) { - done() - } - }) - - parser.on('packet', function (packet) { - var packets = [] - - if (packet.cmd === 'connect') { - duplex.push(mqttPacket.generate({ - cmd: 'connack', - sessionPresent: false, - returnCode: 0 - })) - - for (var i = 0; i < max; i++) { - packets.push(mqttPacket.generate({ - cmd: 'publish', - topic: Buffer.from('hello'), - payload: Buffer.from('world'), - retain: false, - dup: false, - messageId: i + 1, - qos: 1 - })) - } - - duplex.push(Buffer.concat(packets)) - } - }) - }) - }) - - describe('flushing', function () { - it('should attempt to complete pending unsub and send on ping timeout', function (done) { - this.timeout(10000) - var server3 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({returnCode: 0}) - }) - }).listen(ports.PORTAND72) - - var pubCallbackCalled = false - var unsubscribeCallbackCalled = false - client = mqtt.connect({ - port: ports.PORTAND72, - host: 'localhost', - keepalive: 1, - connectTimeout: 350, - reconnectPeriod: 0 - }) - client.once('connect', () => { - client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { - assert.exists(err) - pubCallbackCalled = true - }) - client.unsubscribe('fakeTopic', (err, result) => { - assert.exists(err) - unsubscribeCallbackCalled = true - }) - setTimeout(() => { - client.end(() => { - assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') - server3.close() - done() - }) - }, 5000) - }) - }) - }) - - describe('reconnecting', function () { - it('should attempt to reconnect once server is down', function (done) { - this.timeout(30000) - - var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) - innerServer.on('close', (code) => { - if (code) { - done(util.format('child process closed with code %d', code)) - } - }) - - innerServer.on('exit', (code) => { - if (code) { - done(util.format('child process exited with code %d', code)) - } - }) - - client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) - client.once('connect', function () { - innerServer.kill('SIGINT') // mocks server shutdown - client.once('close', function () { - assert.exists(client.reconnectTimer) - client.end(true, done) - }) - }) - }) - - it('should reconnect if a connack is not received in an interval', function (done) { - this.timeout(2000) - - var server2 = net.createServer().listen(ports.PORTAND43) - - server2.on('connection', function (c) { - eos(c, function () { - server2.close() - }) - }) - - server2.on('listening', function () { - client = mqtt.connect({ - servers: [ - { port: ports.PORTAND43, host: 'localhost_fake' }, - { port: ports.PORT, host: 'localhost' } - ], - connectTimeout: 500 - }) - - server.once('client', function () { - client.end(true, (err) => { - done(err) - }) - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - - it('should not be cleared by the connack timer', function (done) { - this.timeout(4000) - - var server2 = net.createServer().listen(ports.PORTAND44) - - server2.on('connection', function (c) { - c.destroy() - }) - - server2.once('listening', function () { - var reconnects = 0 - var connectTimeout = 1000 - var reconnectPeriod = 100 - var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) - client = mqtt.connect({ - port: ports.PORTAND44, - host: 'localhost', - connectTimeout: connectTimeout, - reconnectPeriod: reconnectPeriod - }) - - client.on('reconnect', function () { - reconnects++ - if (reconnects >= expectedReconnects) { - client.end(true, done) - } - }) - }) - }) - - it('should not keep requeueing the first message when offline', function (done) { - this.timeout(2500) - - var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) - client = mqtt.connect({ - port: ports.PORTAND45, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - server2.on('client', function (serverClient) { - client.publish('hello', 'world', { qos: 1 }, function () { - serverClient.destroy() - server2.close(() => { - debug('now publishing message in an offline state') - client.publish('hello', 'world', { qos: 1 }) - }) - }) - }) - - setTimeout(function () { - if (client.queue.length === 0) { - debug('calling final client.end()') - client.end(true, (err) => done(err)) - } else { - debug('calling client.end()') - client.end(true) - } - }, 2000) - }) - - it('should not send the same subscribe multiple times on a flaky connection', function (done) { - this.timeout(3500) - - var KILL_COUNT = 4 - var killedConnections = 0 - var subIds = {} - client = mqtt.connect({ - port: ports.PORTAND46, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - var server2 = new MqttServer(function (serverClient) { - serverClient.on('error', function () {}) - debug('setting serverClient connect callback') - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - debug('connack with returnCode 2') - serverClient.connack({returnCode: 2}) - } else { - debug('connack with returnCode 0') - serverClient.connack({returnCode: 0}) - } - }) - }).listen(ports.PORTAND46) - - server2.on('client', function (serverClient) { - debug('client received on server2.') - debug('subscribing to topic `topic`') - client.subscribe('topic', function () { - debug('once subscribed to topic, end client, destroy serverClient, and close server.') - serverClient.destroy() - server2.close(() => { client.end(true, done) }) - }) - - serverClient.on('subscribe', function (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few sub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - } else { - // Keep track of acks - if (!subIds[packet.messageId]) { - subIds[packet.messageId] = 0 - } - subIds[packet.messageId]++ - if (subIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) - client.end(true) - serverClient.end() - server2.destroy() - } - - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } - }) - }) - }) - - it('should not fill the queue of subscribes if it cannot connect', function (done) { - this.timeout(2500) - var server2 = net.createServer(function (stream) { - var serverClient = new Connection(stream) - - serverClient.on('error', function (e) { /* do nothing */ }) - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - serverClient.destroy() - }) - }) - - server2.listen(ports.PORTAND48, function () { - client = mqtt.connect({ - port: ports.PORTAND48, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - client.subscribe('hello') - - setTimeout(function () { - assert.equal(client.queue.length, 1) - client.end(true, () => { - done() - }) - }, 1000) - }) - }) - - it('should not send the same publish multiple times on a flaky connection', function (done) { - this.timeout(3500) - - var KILL_COUNT = 4 - var killedConnections = 0 - var pubIds = {} - client = mqtt.connect({ - port: ports.PORTAND47, - host: 'localhost', - connectTimeout: 350, - reconnectPeriod: 300 - }) - - var server2 = net.createServer(function (stream) { - var serverClient = new Connection(stream) - serverClient.on('error', function () {}) - serverClient.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - serverClient.connack({returnCode: 2}) - } else { - serverClient.connack({returnCode: 0}) - } - }) - - this.emit('client', serverClient) - }).listen(ports.PORTAND47) - - server2.on('client', function (serverClient) { - client.publish('topic', 'data', { qos: 1 }, function () { - serverClient.destroy() - server2.close() - client.end(true, done) - }) - - serverClient.on('publish', function onPublish (packet) { - if (killedConnections < KILL_COUNT) { - // Kill the first few pub attempts to simulate a flaky connection - killedConnections++ - serverClient.destroy() - - // to avoid receiving inflight messages - serverClient.removeListener('publish', onPublish) - } else { - // Keep track of acks - if (!pubIds[packet.messageId]) { - pubIds[packet.messageId] = 0 - } - - pubIds[packet.messageId]++ - - if (pubIds[packet.messageId] > 1) { - done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) - client.end(true) - serverClient.destroy() - server2.destroy() - } - - serverClient.puback(packet) - } - }) - }) - }) - }) - - it('check emit error on checkDisconnection w/o callback', function (done) { - this.timeout(15000) - - var server118 = new MqttServer(function (client) { - client.on('connect', function (packet) { - client.connack({ - reasonCode: 0 - }) - }) - client.on('publish', function (packet) { - setImmediate(function () { - packet.reasonCode = 0 - client.puback(packet) - }) - }) - }).listen(ports.PORTAND118) - - var opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - client = mqtt.connect(opts) - - // wait for the client to receive an error... - client.on('error', function (error) { - assert.equal(error.message, 'client disconnecting') - server118.close() - done() - }) - client.on('connect', function () { - client.end(function () { - client._checkDisconnecting() - }) - server118.close() - }) - }) -}) +'use strict' + +var mqtt = require('..') +var assert = require('chai').assert +const { fork } = require('child_process') +var path = require('path') +var abstractClientTests = require('./abstract_client') +var net = require('net') +var eos = require('end-of-stream') +var mqttPacket = require('mqtt-packet') +var Duplex = require('readable-stream').Duplex +var Connection = require('mqtt-connection') +var MqttServer = require('./server').MqttServer +var util = require('util') +var ports = require('./helpers/port_list') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var debug = require('debug')('TEST:client') + +describe('MqttClient', function () { + var client + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORT} + server.listen(ports.PORT) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) + + describe('creating', function () { + it('should allow instantiation of MqttClient without the \'new\' operator', function (done) { + try { + client = mqtt.MqttClient(function () { + throw Error('break') + }, {}) + client.end() + } catch (err) { + assert.strictEqual(err.message, 'break') + done() + } + }) + }) + + describe('message ids', function () { + it('should increment the message id', function () { + client = mqtt.connect(config) + var currentId = client._nextId() + + assert.equal(client._nextId(), currentId + 1) + client.end() + }) + + it('should not throw an error if packet\'s messageId is not found when receiving a pubrel packet', function (done) { + var server2 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.pubrel({ messageId: Math.floor(Math.random() * 9000) + 1000 }) + }) + }) + + server2.listen(ports.PORTAND49, function () { + client = mqtt.connect({ + port: ports.PORTAND49, + host: 'localhost' + }) + + client.on('packetsend', function (packet) { + if (packet.cmd === 'pubcomp') { + client.end() + server2.close() + done() + } + }) + }) + }) + + it('should not go overflow if the TCP frame contains a lot of PUBLISH packets', function (done) { + var parser = mqttPacket.parser() + var count = 0 + var max = 1000 + var duplex = new Duplex({ + read: function (n) {}, + write: function (chunk, enc, cb) { + parser.parse(chunk) + cb() // nothing to do + } + }) + client = new mqtt.MqttClient(function () { + return duplex + }, {}) + + client.on('message', function (t, p, packet) { + if (++count === max) { + done() + } + }) + + parser.on('packet', function (packet) { + var packets = [] + + if (packet.cmd === 'connect') { + duplex.push(mqttPacket.generate({ + cmd: 'connack', + sessionPresent: false, + returnCode: 0 + })) + + for (var i = 0; i < max; i++) { + packets.push(mqttPacket.generate({ + cmd: 'publish', + topic: Buffer.from('hello'), + payload: Buffer.from('world'), + retain: false, + dup: false, + messageId: i + 1, + qos: 1 + })) + } + + duplex.push(Buffer.concat(packets)) + } + }) + }) + }) + + describe('flushing', function () { + it('should attempt to complete pending unsub and send on ping timeout', function (done) { + this.timeout(10000) + var server3 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({returnCode: 0}) + }) + }).listen(ports.PORTAND72) + + var pubCallbackCalled = false + var unsubscribeCallbackCalled = false + client = mqtt.connect({ + port: ports.PORTAND72, + host: 'localhost', + keepalive: 1, + connectTimeout: 350, + reconnectPeriod: 0 + }) + client.once('connect', () => { + client.publish('fakeTopic', 'fakeMessage', {qos: 1}, (err, result) => { + assert.exists(err) + pubCallbackCalled = true + }) + client.unsubscribe('fakeTopic', (err, result) => { + assert.exists(err) + unsubscribeCallbackCalled = true + }) + setTimeout(() => { + client.end(() => { + assert.strictEqual(pubCallbackCalled && unsubscribeCallbackCalled, true, 'callbacks not invoked') + server3.close() + done() + }) + }, 5000) + }) + }) + }) + + describe('reconnecting', function () { + it('should attempt to reconnect once server is down', function (done) { + this.timeout(30000) + + var innerServer = fork(path.join(__dirname, 'helpers', 'server_process.js'), { execArgv: ['--inspect'] }) + innerServer.on('close', (code) => { + if (code) { + done(util.format('child process closed with code %d', code)) + } + }) + + innerServer.on('exit', (code) => { + if (code) { + done(util.format('child process exited with code %d', code)) + } + }) + + client = mqtt.connect({ port: 3481, host: 'localhost', keepalive: 1 }) + client.once('connect', function () { + innerServer.kill('SIGINT') // mocks server shutdown + client.once('close', function () { + assert.exists(client.reconnectTimer) + client.end(true, done) + }) + }) + }) + + it('should reconnect if a connack is not received in an interval', function (done) { + this.timeout(2000) + + var server2 = net.createServer().listen(ports.PORTAND43) + + server2.on('connection', function (c) { + eos(c, function () { + server2.close() + }) + }) + + server2.on('listening', function () { + client = mqtt.connect({ + servers: [ + { port: ports.PORTAND43, host: 'localhost_fake' }, + { port: ports.PORT, host: 'localhost' } + ], + connectTimeout: 500 + }) + + server.once('client', function () { + client.end(true, (err) => { + done(err) + }) + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + + it('should not be cleared by the connack timer', function (done) { + this.timeout(4000) + + var server2 = net.createServer().listen(ports.PORTAND44) + + server2.on('connection', function (c) { + c.destroy() + }) + + server2.once('listening', function () { + var reconnects = 0 + var connectTimeout = 1000 + var reconnectPeriod = 100 + var expectedReconnects = Math.floor(connectTimeout / reconnectPeriod) + client = mqtt.connect({ + port: ports.PORTAND44, + host: 'localhost', + connectTimeout: connectTimeout, + reconnectPeriod: reconnectPeriod + }) + + client.on('reconnect', function () { + reconnects++ + if (reconnects >= expectedReconnects) { + client.end(true, done) + } + }) + }) + }) + + it('should not keep requeueing the first message when offline', function (done) { + this.timeout(2500) + + var server2 = serverBuilder('mqtt').listen(ports.PORTAND45) + client = mqtt.connect({ + port: ports.PORTAND45, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + server2.on('client', function (serverClient) { + client.publish('hello', 'world', { qos: 1 }, function () { + serverClient.destroy() + server2.close(() => { + debug('now publishing message in an offline state') + client.publish('hello', 'world', { qos: 1 }) + }) + }) + }) + + setTimeout(function () { + if (client.queue.length === 0) { + debug('calling final client.end()') + client.end(true, (err) => done(err)) + } else { + debug('calling client.end()') + client.end(true) + } + }, 2000) + }) + + it('should not send the same subscribe multiple times on a flaky connection', function (done) { + this.timeout(3500) + + var KILL_COUNT = 4 + var killedConnections = 0 + var subIds = {} + client = mqtt.connect({ + port: ports.PORTAND46, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + var server2 = new MqttServer(function (serverClient) { + serverClient.on('error', function () {}) + debug('setting serverClient connect callback') + serverClient.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + debug('connack with returnCode 2') + serverClient.connack({returnCode: 2}) + } else { + debug('connack with returnCode 0') + serverClient.connack({returnCode: 0}) + } + }) + }).listen(ports.PORTAND46) + + server2.on('client', function (serverClient) { + debug('client received on server2.') + debug('subscribing to topic `topic`') + client.subscribe('topic', function () { + debug('once subscribed to topic, end client, destroy serverClient, and close server.') + serverClient.destroy() + server2.close(() => { client.end(true, done) }) + }) + + serverClient.on('subscribe', function (packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few sub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + } else { + // Keep track of acks + if (!subIds[packet.messageId]) { + subIds[packet.messageId] = 0 + } + subIds[packet.messageId]++ + if (subIds[packet.messageId] > 1) { + done(new Error('Multiple duplicate acked subscriptions received for messageId ' + packet.messageId)) + client.end(true) + serverClient.end() + server2.destroy() + } + + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } + }) + }) + }) + + it('should not fill the queue of subscribes if it cannot connect', function (done) { + this.timeout(2500) + var server2 = net.createServer(function (stream) { + var serverClient = new Connection(stream) + + serverClient.on('error', function (e) { /* do nothing */ }) + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + serverClient.destroy() + }) + }) + + server2.listen(ports.PORTAND48, function () { + client = mqtt.connect({ + port: ports.PORTAND48, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + client.subscribe('hello') + + setTimeout(function () { + assert.equal(client.queue.length, 1) + client.end(true, () => { + done() + }) + }, 1000) + }) + }) + + it('should not send the same publish multiple times on a flaky connection', function (done) { + this.timeout(3500) + + var KILL_COUNT = 4 + var killedConnections = 0 + var pubIds = {} + client = mqtt.connect({ + port: ports.PORTAND47, + host: 'localhost', + connectTimeout: 350, + reconnectPeriod: 300 + }) + + var server2 = net.createServer(function (stream) { + var serverClient = new Connection(stream) + serverClient.on('error', function () {}) + serverClient.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + serverClient.connack({returnCode: 2}) + } else { + serverClient.connack({returnCode: 0}) + } + }) + + this.emit('client', serverClient) + }).listen(ports.PORTAND47) + + server2.on('client', function (serverClient) { + client.publish('topic', 'data', { qos: 1 }, function () { + serverClient.destroy() + server2.close() + client.end(true, done) + }) + + serverClient.on('publish', function onPublish (packet) { + if (killedConnections < KILL_COUNT) { + // Kill the first few pub attempts to simulate a flaky connection + killedConnections++ + serverClient.destroy() + + // to avoid receiving inflight messages + serverClient.removeListener('publish', onPublish) + } else { + // Keep track of acks + if (!pubIds[packet.messageId]) { + pubIds[packet.messageId] = 0 + } + + pubIds[packet.messageId]++ + + if (pubIds[packet.messageId] > 1) { + done(new Error('Multiple duplicate acked publishes received for messageId ' + packet.messageId)) + client.end(true) + serverClient.destroy() + server2.destroy() + } + + serverClient.puback(packet) + } + }) + }) + }) + }) + + it('check emit error on checkDisconnection w/o callback', function (done) { + this.timeout(15000) + + var server118 = new MqttServer(function (client) { + client.on('connect', function (packet) { + client.connack({ + reasonCode: 0 + }) + }) + client.on('publish', function (packet) { + setImmediate(function () { + packet.reasonCode = 0 + client.puback(packet) + }) + }) + }).listen(ports.PORTAND118) + + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + client = mqtt.connect(opts) + + // wait for the client to receive an error... + client.on('error', function (error) { + assert.equal(error.message, 'client disconnecting') + server118.close() + done() + }) + client.on('connect', function () { + client.end(function () { + client._checkDisconnecting() + }) + server118.close() + }) + }) +}) diff --git a/test/client_mqtt5.js b/test/client_mqtt5.js index 0fe2ecb88..fd2bb9979 100644 --- a/test/client_mqtt5.js +++ b/test/client_mqtt5.js @@ -1,1053 +1,1053 @@ -'use strict' - -var mqtt = require('..') -var abstractClientTests = require('./abstract_client') -var MqttServer = require('./server').MqttServer -var assert = require('chai').assert -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var ports = require('./helpers/port_list') - -describe('MQTT 5.0', function () { - var server = serverBuilder('mqtt').listen(ports.PORTAND115) - var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } - - abstractClientTests(server, config) - - it('topic should be complemented on receive', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - assert.strictEqual(packet.properties.topicAliasMaximum, 3) - serverClient.connack({ - reasonCode: 0 - }) - // register topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // overwrite registered topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test2', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - // use topicAlias - serverClient.publish({ - messageId: 0, - topic: '', - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('message', function (topic, messagee, packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(topic, 'test1') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 3: - assert.strictEqual(topic, 'test2') - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - server103.close() - client.end(true, done) - break - } - }) - }) - - it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoUseTopicAlias: true - } - var client = mqtt.connect(opts) - - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 2: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { properties: { topicAlias: 1 } }) - // use topicAlias by autoApplyTopicAlias - client.publish('test1', 'Message') - }) - }) - - it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - autoAssignTopicAlias: true - } - var client = mqtt.connect(opts) - - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - topicAliasMaximum: 3 - } - }) - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, 'test2') - assert.strictEqual(packet.properties.topicAlias, 2) - break - case 2: - assert.strictEqual(packet.topic, 'test3') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 3: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 4: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 3) - break - case 5: - assert.strictEqual(packet.topic, 'test4') - assert.strictEqual(packet.properties.topicAlias, 2) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish('test1', 'Message') - client.publish('test2', 'Message') - client.publish('test3', 'Message') - - // use topicAlias - client.publish('test1', 'Message') - client.publish('test3', 'Message') - - // renew LRU topicAlias - client.publish('test4', 'Message') - }) - }) - - it('topicAlias should be removed and topic restored on resend', function (done) { - this.timeout(15000) - - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore: incomingStore, - outgoingStore: outgoingStore, - clean: false, - reconnectPeriod: 100 - } - var client = mqtt.connect(opts) - - var connectCount = 0 - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - assert.strictEqual(packet.properties.topicAlias, 1) - break - case 1: - assert.strictEqual(packet.topic, '') - assert.strictEqual(packet.properties.topicAlias, 1) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 2: - assert.strictEqual(packet.topic, 'test1') - var alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - serverClient.puback({messageId: packet.messageId}) - break - case 3: - assert.strictEqual(packet.topic, 'test1') - var alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - serverClient.puback({messageId: packet.messageId}) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.once('connect', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('topicAlias should be removed and topic restored on offline publish', function (done) { - this.timeout(15000) - - var incomingStore = new mqtt.Store({ clean: false }) - var outgoingStore = new mqtt.Store({ clean: false }) - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - clientId: 'cid1', - incomingStore: incomingStore, - outgoingStore: outgoingStore, - clean: false, - reconnectPeriod: 100 - } - var client = mqtt.connect(opts) - - var connectCount = 0 - var publishCount = 0 - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - switch (connectCount++) { - case 0: - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - setImmediate(function () { - serverClient.stream.destroy() - }) - break - case 1: - serverClient.connack({ - reasonCode: 0, - sessionPresent: true, - properties: { - topicAliasMaximum: 3 - } - }) - break - } - }) - serverClient.on('publish', function (packet) { - switch (publishCount++) { - case 0: - assert.strictEqual(packet.topic, 'test1') - var alias1 - if (packet.properties) { - alias1 = packet.properties.topicAlias - } - assert.strictEqual(alias1, undefined) - assert.strictEqual(packet.qos, 1) - serverClient.puback({messageId: packet.messageId}) - break - case 1: - assert.strictEqual(packet.topic, 'test1') - var alias2 - if (packet.properties) { - alias2 = packet.properties.topicAlias - } - assert.strictEqual(alias2, undefined) - assert.strictEqual(packet.qos, 0) - break - case 2: - assert.strictEqual(packet.topic, 'test1') - var alias3 - if (packet.properties) { - alias3 = packet.properties.topicAlias - } - assert.strictEqual(alias3, undefined) - assert.strictEqual(packet.qos, 0) - server103.close() - client.end(true, done) - break - } - }) - }).listen(ports.PORTAND103) - - client.once('close', function () { - // register topicAlias - client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - // use topicAlias - client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) - client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false, - properties: { - topicAliasMaximum: 3 - } - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 4 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - server103.close() - client.end(true, done) - }) - }) - }) - - it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - }).listen(ports.PORTAND103) - - client.on('connect', function () { - // register topicAlias - client.publish( - 'test1', - 'Message', - { properties: { topicAlias: 1 } }, - function (error) { - assert.strictEqual(error.message, 'Sending Topic Alias out of range') - server103.close() - client.end(true, done) - }) - }) - }) - - it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 4 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if broker PUBLISH topicAlias:0', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: 'test1', - payload: 'Message', - qos: 0, - properties: { topicAlias: 0 } - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received Topic Alias is out of range') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { - this.timeout(15000) - - var opts = { - host: 'localhost', - port: ports.PORTAND103, - protocolVersion: 5, - topicAliasMaximum: 3 - } - var client = mqtt.connect(opts) - var server103 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - // register out of range topicAlias - serverClient.publish({ - messageId: 0, - topic: '', // use topic alias - payload: 'Message', - qos: 0, - properties: { topicAlias: 1 } // in range topic alias - }) - }) - }).listen(ports.PORTAND103) - - client.on('error', function (error) { - assert.strictEqual(error.message, 'Received unregistered Topic Alias') - server103.close() - client.end(true, done) - }) - }) - - it('should throw an error if there is Auth Data with no Auth Method', function (done) { - this.timeout(5000) - var client - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} - console.log('client connecting') - client = mqtt.connect(opts) - client.on('error', function (error) { - console.log('error hit') - assert.strictEqual(error.message, 'Packet has no Authentication Method') - // client will not be connected, so we will call done. - assert.isTrue(client.disconnected, 'validate client is disconnected') - client.end(true, done) - }) - }) - - it('auth packet', function (done) { - this.timeout(15000) - server.once('client', function (serverClient) { - console.log('server received client') - serverClient.on('auth', function (packet) { - console.log('serverClient received auth: packet %o', packet) - serverClient.end(done) - }) - }) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} - console.log('calling mqtt connect') - mqtt.connect(opts) - }) - - it('Maximum Packet Size', function (done) { - this.timeout(15000) - var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'exceeding packets size connack') - client.end(true, done) - }) - }) - - it('Change values of some properties by server response', function (done) { - this.timeout(15000) - var server116 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - properties: { - serverKeepAlive: 16, - maximumPacketSize: 95 - } - }) - }) - }).listen(ports.PORTAND116) - var opts = { - host: 'localhost', - port: ports.PORTAND116, - protocolVersion: 5, - properties: { - topicAliasMaximum: 10, - serverKeepAlive: 11, - maximumPacketSize: 100 - } - } - var client = mqtt.connect(opts) - client.on('connect', function () { - assert.strictEqual(client.options.keepalive, 16) - assert.strictEqual(client.options.properties.maximumPacketSize, 95) - server116.close() - client.end(true, done) - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { - this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server316 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - serverClient.on('subscribe', function () { - if (!tryReconnect) { - server316.close() - serverClient.end(done) - } - }) - }) - }).listen(ports.PORTAND316) - var opts = { - host: 'localhost', - port: ports.PORTAND316, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { - // this.timeout(15000) - var tryReconnect = true - var reconnectEvent = false - var server326 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0, - sessionPresent: false - }) - }) - serverClient.on('subscribe', function (packet) { - if (!reconnectEvent) { - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - } else { - if (!tryReconnect) { - assert.strictEqual(packet.properties.userProperties.test, 'test') - serverClient.end(done) - server326.close() - } - } - }) - }).listen(ports.PORTAND326) - - var opts = { - host: 'localhost', - port: ports.PORTAND326, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - - client.on('reconnect', function () { - reconnectEvent = true - }) - - client.on('connect', function (connack) { - assert.isFalse(connack.sessionPresent) - if (tryReconnect) { - client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { - client.stream.end() - }) - - tryReconnect = false - } else { - assert.isTrue(reconnectEvent) - } - }) - }) - - var serverThatSendsErrors = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - packet.reasonCode = 142 - delete packet.cmd - serverClient.puback(packet) - break - case 2: - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - packet.reasonCode = 142 - delete packet.cmd - serverClient.pubcomp(packet) - }) - }) - - it('Subscribe properties', function (done) { - this.timeout(15000) - var opts = { - host: 'localhost', - port: ports.PORTAND119, - protocolVersion: 5 - } - var subOptions = { properties: { subscriptionIdentifier: 1234 } } - var server119 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - }) - serverClient.on('subscribe', function (packet) { - assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) - server119.close() - serverClient.end() - done() - }) - }).listen(ports.PORTAND119) - - var client = mqtt.connect(opts) - client.on('connect', function () { - client.subscribe('a/b', subOptions) - }) - }) - - it('puback handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 1}, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - serverThatSendsErrors.close() - client.end(true, done) - }) - }) - - it('pubrec handling errors check', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND118) - var opts = { - host: 'localhost', - port: ports.PORTAND118, - protocolVersion: 5 - } - var client = mqtt.connect(opts) - client.once('connect', () => { - client.publish('a/b', 'message', {qos: 2}, function (err, packet) { - assert.strictEqual(err.message, 'Publish error: Session taken over') - assert.strictEqual(err.code, 142) - }) - serverThatSendsErrors.close() - client.end(true, done) - }) - }) - - it('puback handling custom reason code', function (done) { - // this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - - serverClient.on('puback', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - serverClient.end(done) - serverClient.destroy() - serverThatSendsErrors.close() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('server side disconnect', function (done) { - this.timeout(15000) - var server327 = new MqttServer(function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({ - reasonCode: 0 - }) - serverClient.disconnect({reasonCode: 128}) - server327.close() - }) - }) - server327.listen(ports.PORTAND327) - var opts = { - host: 'localhost', - port: ports.PORTAND327, - protocolVersion: 5 - } - - var client = mqtt.connect(opts) - client.once('disconnect', function (disconnectPacket) { - assert.strictEqual(disconnectPacket.reasonCode, 128) - client.end(true, done) - }) - }) - - it('pubrec handling custom reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 128 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - - serverClient.on('pubrec', function (packet) { - assert.strictEqual(packet.reasonCode, 128) - client.end(true, done) - serverClient.destroy() - serverThatSendsErrors.close() - }) - }) - - var client = mqtt.connect(opts) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('puback handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('pubrec handling custom reason code with error', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - cb(new Error('a/b is not valid')) - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'a/b is not valid') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('puback handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 124124 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for puback') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) - - it('pubrec handling custom invalid reason code', function (done) { - this.timeout(15000) - serverThatSendsErrors.listen(ports.PORTAND117) - var opts = { - host: 'localhost', - port: ports.PORTAND117, - protocolVersion: 5, - customHandleAcks: function (topic, message, packet, cb) { - var code = 0 - if (topic === 'a/b') { - code = 34535 - } - cb(code) - } - } - - serverThatSendsErrors.once('client', function (serverClient) { - serverClient.once('subscribe', function () { - serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) - }) - }) - - var client = mqtt.connect(opts) - client.on('error', function (error) { - assert.strictEqual(error.message, 'Wrong reason code for pubrec') - client.end(true, done) - serverThatSendsErrors.close() - }) - client.once('connect', function () { - client.subscribe('a/b', {qos: 1}) - }) - }) -}) +'use strict' + +var mqtt = require('..') +var abstractClientTests = require('./abstract_client') +var MqttServer = require('./server').MqttServer +var assert = require('chai').assert +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var ports = require('./helpers/port_list') + +describe('MQTT 5.0', function () { + var server = serverBuilder('mqtt').listen(ports.PORTAND115) + var config = { protocol: 'mqtt', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 200 } } + + abstractClientTests(server, config) + + it('topic should be complemented on receive', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + assert.strictEqual(packet.properties.topicAliasMaximum, 3) + serverClient.connack({ + reasonCode: 0 + }) + // register topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // overwrite registered topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test2', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + // use topicAlias + serverClient.publish({ + messageId: 0, + topic: '', + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('message', function (topic, messagee, packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(topic, 'test1') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 3: + assert.strictEqual(topic, 'test2') + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }) + + it('registered topic alias should automatically used if autoUseTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoUseTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 2: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { properties: { topicAlias: 1 } }) + // use topicAlias by autoApplyTopicAlias + client.publish('test1', 'Message') + }) + }) + + it('topicAlias is automatically used if autoAssignTopicAlias is true', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + autoAssignTopicAlias: true + } + var client = mqtt.connect(opts) + + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + topicAliasMaximum: 3 + } + }) + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, 'test2') + assert.strictEqual(packet.properties.topicAlias, 2) + break + case 2: + assert.strictEqual(packet.topic, 'test3') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 3: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 4: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 3) + break + case 5: + assert.strictEqual(packet.topic, 'test4') + assert.strictEqual(packet.properties.topicAlias, 2) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish('test1', 'Message') + client.publish('test2', 'Message') + client.publish('test3', 'Message') + + // use topicAlias + client.publish('test1', 'Message') + client.publish('test3', 'Message') + + // renew LRU topicAlias + client.publish('test4', 'Message') + }) + }) + + it('topicAlias should be removed and topic restored on resend', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + assert.strictEqual(packet.properties.topicAlias, 1) + break + case 1: + assert.strictEqual(packet.topic, '') + assert.strictEqual(packet.properties.topicAlias, 1) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + serverClient.puback({messageId: packet.messageId}) + break + case 3: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + serverClient.puback({messageId: packet.messageId}) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('connect', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('topicAlias should be removed and topic restored on offline publish', function (done) { + this.timeout(15000) + + var incomingStore = new mqtt.Store({ clean: false }) + var outgoingStore = new mqtt.Store({ clean: false }) + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + clientId: 'cid1', + incomingStore: incomingStore, + outgoingStore: outgoingStore, + clean: false, + reconnectPeriod: 100 + } + var client = mqtt.connect(opts) + + var connectCount = 0 + var publishCount = 0 + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + switch (connectCount++) { + case 0: + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + setImmediate(function () { + serverClient.stream.destroy() + }) + break + case 1: + serverClient.connack({ + reasonCode: 0, + sessionPresent: true, + properties: { + topicAliasMaximum: 3 + } + }) + break + } + }) + serverClient.on('publish', function (packet) { + switch (publishCount++) { + case 0: + assert.strictEqual(packet.topic, 'test1') + var alias1 + if (packet.properties) { + alias1 = packet.properties.topicAlias + } + assert.strictEqual(alias1, undefined) + assert.strictEqual(packet.qos, 1) + serverClient.puback({messageId: packet.messageId}) + break + case 1: + assert.strictEqual(packet.topic, 'test1') + var alias2 + if (packet.properties) { + alias2 = packet.properties.topicAlias + } + assert.strictEqual(alias2, undefined) + assert.strictEqual(packet.qos, 0) + break + case 2: + assert.strictEqual(packet.topic, 'test1') + var alias3 + if (packet.properties) { + alias3 = packet.properties.topicAlias + } + assert.strictEqual(alias3, undefined) + assert.strictEqual(packet.qos, 0) + server103.close() + client.end(true, done) + break + } + }) + }).listen(ports.PORTAND103) + + client.once('close', function () { + // register topicAlias + client.publish('test1', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + // use topicAlias + client.publish('', 'Message', { qos: 0, properties: { topicAlias: 1 } }) + client.publish('', 'Message', { qos: 1, properties: { topicAlias: 1 } }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false, + properties: { + topicAliasMaximum: 3 + } + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 4 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should error cb call if PUBLISH out of range topicAlias on topicAlias disabled by broker', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + }).listen(ports.PORTAND103) + + client.on('connect', function () { + // register topicAlias + client.publish( + 'test1', + 'Message', + { properties: { topicAlias: 1 } }, + function (error) { + assert.strictEqual(error.message, 'Sending Topic Alias out of range') + server103.close() + client.end(true, done) + }) + }) + }) + + it('should throw an error if broker PUBLISH out of range topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 4 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH topicAlias:0', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: 'test1', + payload: 'Message', + qos: 0, + properties: { topicAlias: 0 } + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received Topic Alias is out of range') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if broker PUBLISH unregistered topicAlias', function (done) { + this.timeout(15000) + + var opts = { + host: 'localhost', + port: ports.PORTAND103, + protocolVersion: 5, + topicAliasMaximum: 3 + } + var client = mqtt.connect(opts) + var server103 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + // register out of range topicAlias + serverClient.publish({ + messageId: 0, + topic: '', // use topic alias + payload: 'Message', + qos: 0, + properties: { topicAlias: 1 } // in range topic alias + }) + }) + }).listen(ports.PORTAND103) + + client.on('error', function (error) { + assert.strictEqual(error.message, 'Received unregistered Topic Alias') + server103.close() + client.end(true, done) + }) + }) + + it('should throw an error if there is Auth Data with no Auth Method', function (done) { + this.timeout(5000) + var client + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationData: Buffer.from([1, 2, 3, 4]) }} + console.log('client connecting') + client = mqtt.connect(opts) + client.on('error', function (error) { + console.log('error hit') + assert.strictEqual(error.message, 'Packet has no Authentication Method') + // client will not be connected, so we will call done. + assert.isTrue(client.disconnected, 'validate client is disconnected') + client.end(true, done) + }) + }) + + it('auth packet', function (done) { + this.timeout(15000) + server.once('client', function (serverClient) { + console.log('server received client') + serverClient.on('auth', function (packet) { + console.log('serverClient received auth: packet %o', packet) + serverClient.end(done) + }) + }) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { authenticationMethod: 'json' }, authPacket: {}} + console.log('calling mqtt connect') + mqtt.connect(opts) + }) + + it('Maximum Packet Size', function (done) { + this.timeout(15000) + var opts = {host: 'localhost', port: ports.PORTAND115, protocolVersion: 5, properties: { maximumPacketSize: 1 }} + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'exceeding packets size connack') + client.end(true, done) + }) + }) + + it('Change values of some properties by server response', function (done) { + this.timeout(15000) + var server116 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + properties: { + serverKeepAlive: 16, + maximumPacketSize: 95 + } + }) + }) + }).listen(ports.PORTAND116) + var opts = { + host: 'localhost', + port: ports.PORTAND116, + protocolVersion: 5, + properties: { + topicAliasMaximum: 10, + serverKeepAlive: 11, + maximumPacketSize: 100 + } + } + var client = mqtt.connect(opts) + client.on('connect', function () { + assert.strictEqual(client.options.keepalive, 16) + assert.strictEqual(client.options.properties.maximumPacketSize, 95) + server116.close() + client.end(true, done) + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and Session Present flag is false', function (done) { + this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server316 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + serverClient.on('subscribe', function () { + if (!tryReconnect) { + server316.close() + serverClient.end(done) + } + }) + }) + }).listen(ports.PORTAND316) + var opts = { + host: 'localhost', + port: ports.PORTAND316, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + it('should resubscribe when reconnecting with protocolVersion 5 and properties', function (done) { + // this.timeout(15000) + var tryReconnect = true + var reconnectEvent = false + var server326 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0, + sessionPresent: false + }) + }) + serverClient.on('subscribe', function (packet) { + if (!reconnectEvent) { + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + } else { + if (!tryReconnect) { + assert.strictEqual(packet.properties.userProperties.test, 'test') + serverClient.end(done) + server326.close() + } + } + }) + }).listen(ports.PORTAND326) + + var opts = { + host: 'localhost', + port: ports.PORTAND326, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + + client.on('reconnect', function () { + reconnectEvent = true + }) + + client.on('connect', function (connack) { + assert.isFalse(connack.sessionPresent) + if (tryReconnect) { + client.subscribe('hello', { properties: { userProperties: { test: 'test' } } }, function () { + client.stream.end() + }) + + tryReconnect = false + } else { + assert.isTrue(reconnectEvent) + } + }) + }) + + var serverThatSendsErrors = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + packet.reasonCode = 142 + delete packet.cmd + serverClient.puback(packet) + break + case 2: + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + packet.reasonCode = 142 + delete packet.cmd + serverClient.pubcomp(packet) + }) + }) + + it('Subscribe properties', function (done) { + this.timeout(15000) + var opts = { + host: 'localhost', + port: ports.PORTAND119, + protocolVersion: 5 + } + var subOptions = { properties: { subscriptionIdentifier: 1234 } } + var server119 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + }) + serverClient.on('subscribe', function (packet) { + assert.strictEqual(packet.properties.subscriptionIdentifier, subOptions.properties.subscriptionIdentifier) + server119.close() + serverClient.end() + done() + }) + }).listen(ports.PORTAND119) + + var client = mqtt.connect(opts) + client.on('connect', function () { + client.subscribe('a/b', subOptions) + }) + }) + + it('puback handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 1}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('pubrec handling errors check', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND118) + var opts = { + host: 'localhost', + port: ports.PORTAND118, + protocolVersion: 5 + } + var client = mqtt.connect(opts) + client.once('connect', () => { + client.publish('a/b', 'message', {qos: 2}, function (err, packet) { + assert.strictEqual(err.message, 'Publish error: Session taken over') + assert.strictEqual(err.code, 142) + }) + serverThatSendsErrors.close() + client.end(true, done) + }) + }) + + it('puback handling custom reason code', function (done) { + // this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + + serverClient.on('puback', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + serverClient.end(done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('server side disconnect', function (done) { + this.timeout(15000) + var server327 = new MqttServer(function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({ + reasonCode: 0 + }) + serverClient.disconnect({reasonCode: 128}) + server327.close() + }) + }) + server327.listen(ports.PORTAND327) + var opts = { + host: 'localhost', + port: ports.PORTAND327, + protocolVersion: 5 + } + + var client = mqtt.connect(opts) + client.once('disconnect', function (disconnectPacket) { + assert.strictEqual(disconnectPacket.reasonCode, 128) + client.end(true, done) + }) + }) + + it('pubrec handling custom reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 128 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + + serverClient.on('pubrec', function (packet) { + assert.strictEqual(packet.reasonCode, 128) + client.end(true, done) + serverClient.destroy() + serverThatSendsErrors.close() + }) + }) + + var client = mqtt.connect(opts) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom reason code with error', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + cb(new Error('a/b is not valid')) + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'a/b is not valid') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('puback handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 124124 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 1, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for puback') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) + + it('pubrec handling custom invalid reason code', function (done) { + this.timeout(15000) + serverThatSendsErrors.listen(ports.PORTAND117) + var opts = { + host: 'localhost', + port: ports.PORTAND117, + protocolVersion: 5, + customHandleAcks: function (topic, message, packet, cb) { + var code = 0 + if (topic === 'a/b') { + code = 34535 + } + cb(code) + } + } + + serverThatSendsErrors.once('client', function (serverClient) { + serverClient.once('subscribe', function () { + serverClient.publish({ topic: 'a/b', payload: 'payload', qos: 2, messageId: 1 }) + }) + }) + + var client = mqtt.connect(opts) + client.on('error', function (error) { + assert.strictEqual(error.message, 'Wrong reason code for pubrec') + client.end(true, done) + serverThatSendsErrors.close() + }) + client.once('connect', function () { + client.subscribe('a/b', {qos: 1}) + }) + }) +}) diff --git a/test/helpers/port_list.js b/test/helpers/port_list.js index d11b8df21..dc77ef07a 100644 --- a/test/helpers/port_list.js +++ b/test/helpers/port_list.js @@ -1,51 +1,51 @@ -var PORT = 9876 -var PORTAND40 = PORT + 40 -var PORTAND41 = PORT + 41 -var PORTAND42 = PORT + 42 -var PORTAND43 = PORT + 43 -var PORTAND44 = PORT + 44 -var PORTAND45 = PORT + 45 -var PORTAND46 = PORT + 46 -var PORTAND47 = PORT + 47 -var PORTAND48 = PORT + 48 -var PORTAND49 = PORT + 49 -var PORTAND50 = PORT + 50 -var PORTAND72 = PORT + 72 -var PORTAND103 = PORT + 103 -var PORTAND114 = PORT + 114 -var PORTAND115 = PORT + 115 -var PORTAND116 = PORT + 116 -var PORTAND117 = PORT + 117 -var PORTAND118 = PORT + 118 -var PORTAND119 = PORT + 119 -var PORTAND316 = PORT + 316 -var PORTAND326 = PORT + 326 -var PORTAND327 = PORT + 327 -var PORTAND400 = PORT + 400 - -module.exports = { - PORT, - PORTAND40, - PORTAND41, - PORTAND42, - PORTAND43, - PORTAND44, - PORTAND45, - PORTAND46, - PORTAND47, - PORTAND48, - PORTAND49, - PORTAND50, - PORTAND72, - PORTAND103, - PORTAND114, - PORTAND115, - PORTAND116, - PORTAND117, - PORTAND118, - PORTAND119, - PORTAND316, - PORTAND326, - PORTAND327, - PORTAND400 -} +var PORT = 9876 +var PORTAND40 = PORT + 40 +var PORTAND41 = PORT + 41 +var PORTAND42 = PORT + 42 +var PORTAND43 = PORT + 43 +var PORTAND44 = PORT + 44 +var PORTAND45 = PORT + 45 +var PORTAND46 = PORT + 46 +var PORTAND47 = PORT + 47 +var PORTAND48 = PORT + 48 +var PORTAND49 = PORT + 49 +var PORTAND50 = PORT + 50 +var PORTAND72 = PORT + 72 +var PORTAND103 = PORT + 103 +var PORTAND114 = PORT + 114 +var PORTAND115 = PORT + 115 +var PORTAND116 = PORT + 116 +var PORTAND117 = PORT + 117 +var PORTAND118 = PORT + 118 +var PORTAND119 = PORT + 119 +var PORTAND316 = PORT + 316 +var PORTAND326 = PORT + 326 +var PORTAND327 = PORT + 327 +var PORTAND400 = PORT + 400 + +module.exports = { + PORT, + PORTAND40, + PORTAND41, + PORTAND42, + PORTAND43, + PORTAND44, + PORTAND45, + PORTAND46, + PORTAND47, + PORTAND48, + PORTAND49, + PORTAND50, + PORTAND72, + PORTAND103, + PORTAND114, + PORTAND115, + PORTAND116, + PORTAND117, + PORTAND118, + PORTAND119, + PORTAND316, + PORTAND326, + PORTAND327, + PORTAND400 +} diff --git a/test/helpers/server.js b/test/helpers/server.js index d29042d3d..46bd79537 100644 --- a/test/helpers/server.js +++ b/test/helpers/server.js @@ -1,53 +1,53 @@ -'use strict' - -var MqttServer = require('../server').MqttServer -var MqttSecureServer = require('../server').MqttSecureServer -var fs = require('fs') - -module.exports.init_server = function (PORT) { - var server = new MqttServer(function (client) { - client.on('connect', function () { - client.connack(0) - }) - - client.on('publish', function (packet) { - switch (packet.qos) { - case 1: - client.puback({messageId: packet.messageId}) - break - case 2: - client.pubrec({messageId: packet.messageId}) - break - default: - break - } - }) - - client.on('pubrel', function (packet) { - client.pubcomp({messageId: packet.messageId}) - }) - - client.on('pingreq', function () { - client.pingresp() - }) - - client.on('disconnect', function () { - client.stream.end() - }) - }) - server.listen(PORT) - return server -} - -module.exports.init_secure_server = function (port, key, cert) { - var server = new MqttSecureServer({ - key: fs.readFileSync(key), - cert: fs.readFileSync(cert) - }, function (client) { - client.on('connect', function () { - client.connack({returnCode: 0}) - }) - }) - server.listen(port) - return server -} +'use strict' + +var MqttServer = require('../server').MqttServer +var MqttSecureServer = require('../server').MqttSecureServer +var fs = require('fs') + +module.exports.init_server = function (PORT) { + var server = new MqttServer(function (client) { + client.on('connect', function () { + client.connack(0) + }) + + client.on('publish', function (packet) { + switch (packet.qos) { + case 1: + client.puback({messageId: packet.messageId}) + break + case 2: + client.pubrec({messageId: packet.messageId}) + break + default: + break + } + }) + + client.on('pubrel', function (packet) { + client.pubcomp({messageId: packet.messageId}) + }) + + client.on('pingreq', function () { + client.pingresp() + }) + + client.on('disconnect', function () { + client.stream.end() + }) + }) + server.listen(PORT) + return server +} + +module.exports.init_secure_server = function (port, key, cert) { + var server = new MqttSecureServer({ + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) + }, function (client) { + client.on('connect', function () { + client.connack({returnCode: 0}) + }) + }) + server.listen(port) + return server +} diff --git a/test/helpers/server_process.js b/test/helpers/server_process.js index d4c2681b4..1d1095cb3 100644 --- a/test/helpers/server_process.js +++ b/test/helpers/server_process.js @@ -1,9 +1,9 @@ -'use strict' - -var MqttServer = require('../server').MqttServer - -new MqttServer(function (client) { - client.on('connect', function () { - client.connack({ returnCode: 0 }) - }) -}).listen(3481, 'localhost') +'use strict' + +var MqttServer = require('../server').MqttServer + +new MqttServer(function (client) { + client.on('connect', function () { + client.connack({ returnCode: 0 }) + }) +}).listen(3481, 'localhost') diff --git a/test/message-id-provider.js b/test/message-id-provider.js index 667a8296f..2f84bdf35 100644 --- a/test/message-id-provider.js +++ b/test/message-id-provider.js @@ -1,91 +1,91 @@ -'use strict' -var assert = require('chai').assert -var DefaultMessageIdProvider = require('../lib/default-message-id-provider') -var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') - -describe('message id provider', function () { - describe('default', function () { - it('should return 1 once the internal counter reached limit', function () { - var provider = new DefaultMessageIdProvider() - provider.nextId = 65535 - - assert.equal(provider.allocate(), 65535) - assert.equal(provider.allocate(), 1) - }) - - it('should return 65535 for last message id once the internal counter reached limit', function () { - var provider = new DefaultMessageIdProvider() - provider.nextId = 65535 - - assert.equal(provider.allocate(), 65535) - assert.equal(provider.getLastAllocated(), 65535) - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - }) - it('should return true when register with non allocated messageId', function () { - var provider = new DefaultMessageIdProvider() - assert.equal(provider.register(10), true) - }) - }) - describe('unique', function () { - it('should return 1, 2, 3.., when allocate', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - }) - it('should skip registerd messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.register(2), true) - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 3) - }) - it('should return false register allocated messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.register(1), false) - assert.equal(provider.register(5), true) - assert.equal(provider.register(5), false) - }) - it('should retrun correct last messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.register(2), true) - assert.equal(provider.getLastAllocated(), 1) - assert.equal(provider.allocate(), 3) - assert.equal(provider.getLastAllocated(), 3) - }) - it('should be reusable deallocated messageId', function () { - var provider = new UniqueMessageIdProvider() - assert.equal(provider.allocate(), 1) - assert.equal(provider.allocate(), 2) - assert.equal(provider.allocate(), 3) - provider.deallocate(2) - assert.equal(provider.allocate(), 2) - }) - it('should allocate all messageId and then return null', function () { - var provider = new UniqueMessageIdProvider() - for (var i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.deallocate(10000) - assert.equal(provider.allocate(), 10000) - assert.equal(provider.allocate(), null) - }) - it('should all messageId reallocatable after clear', function () { - var provider = new UniqueMessageIdProvider() - var i - for (i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - provider.clear() - for (i = 1; i <= 65535; i++) { - assert.equal(provider.allocate(), i) - } - assert.equal(provider.allocate(), null) - }) - }) -}) +'use strict' +var assert = require('chai').assert +var DefaultMessageIdProvider = require('../lib/default-message-id-provider') +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') + +describe('message id provider', function () { + describe('default', function () { + it('should return 1 once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.allocate(), 1) + }) + + it('should return 65535 for last message id once the internal counter reached limit', function () { + var provider = new DefaultMessageIdProvider() + provider.nextId = 65535 + + assert.equal(provider.allocate(), 65535) + assert.equal(provider.getLastAllocated(), 65535) + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + }) + it('should return true when register with non allocated messageId', function () { + var provider = new DefaultMessageIdProvider() + assert.equal(provider.register(10), true) + }) + }) + describe('unique', function () { + it('should return 1, 2, 3.., when allocate', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + }) + it('should skip registerd messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.register(2), true) + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 3) + }) + it('should return false register allocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.register(1), false) + assert.equal(provider.register(5), true) + assert.equal(provider.register(5), false) + }) + it('should retrun correct last messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.register(2), true) + assert.equal(provider.getLastAllocated(), 1) + assert.equal(provider.allocate(), 3) + assert.equal(provider.getLastAllocated(), 3) + }) + it('should be reusable deallocated messageId', function () { + var provider = new UniqueMessageIdProvider() + assert.equal(provider.allocate(), 1) + assert.equal(provider.allocate(), 2) + assert.equal(provider.allocate(), 3) + provider.deallocate(2) + assert.equal(provider.allocate(), 2) + }) + it('should allocate all messageId and then return null', function () { + var provider = new UniqueMessageIdProvider() + for (var i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.deallocate(10000) + assert.equal(provider.allocate(), 10000) + assert.equal(provider.allocate(), null) + }) + it('should all messageId reallocatable after clear', function () { + var provider = new UniqueMessageIdProvider() + var i + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + provider.clear() + for (i = 1; i <= 65535; i++) { + assert.equal(provider.allocate(), i) + } + assert.equal(provider.allocate(), null) + }) + }) +}) diff --git a/test/mqtt.js b/test/mqtt.js index d3315b69e..f55d04a33 100644 --- a/test/mqtt.js +++ b/test/mqtt.js @@ -1,230 +1,230 @@ -'use strict' - -var fs = require('fs') -var path = require('path') -var mqtt = require('../') - -describe('mqtt', function () { - describe('#connect', function () { - var sslOpts, sslOpts2 - it('should return an MqttClient when connect is called with mqtt:/ url', function () { - var c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should throw an error when called with no protocol specified', function () { - (function () { - var c = mqtt.connect('foo.bar.com') - c.end() - }).should.throw('Missing protocol') - }) - - it('should throw an error when called with no protocol specified - with options', function () { - (function () { - var c = mqtt.connect('tcp://foo.bar.com', { protocol: null }) - c.end() - }).should.throw('Missing protocol') - }) - - it('should return an MqttClient with username option set', function () { - var c = mqtt.connect('mqtt://user:pass@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.options.should.have.property('password', 'pass') - c.end() - }) - - it('should return an MqttClient with username and password options set', function () { - var c = mqtt.connect('mqtt://user@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('username', 'user') - c.end() - }) - - it('should return an MqttClient with the clientid with random value', function () { - var c = mqtt.connect('mqtt://user@localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end() - }) - - it('should return an MqttClient with the clientid with empty string', function () { - var c = mqtt.connect('mqtt://user@localhost:1883?clientId=') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - c.end() - }) - - it('should return an MqttClient with the clientid option set', function () { - var c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end() - }) - - it('should return an MqttClient when connect is called with tcp:/ url', function () { - var c = mqtt.connect('tcp://localhost') - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient with correct host when called with a host and port', function () { - var c = mqtt.connect('tcp://user:pass@localhost:1883') - - c.options.should.have.property('hostname', 'localhost') - c.options.should.have.property('port', 1883) - c.end() - }) - - sslOpts = { - keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), - certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), - caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] - } - - it('should return an MqttClient when connect is called with mqtts:/ url', function () { - var c = mqtt.connect('mqtts://localhost', sslOpts) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with ssl:/ url', function () { - var c = mqtt.connect('ssl://localhost', sslOpts) - - c.options.should.have.property('protocol', 'ssl') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with ws:/ url', function () { - var c = mqtt.connect('ws://localhost', sslOpts) - - c.options.should.have.property('protocol', 'ws') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - it('should return an MqttClient when connect is called with wss:/ url', function () { - var c = mqtt.connect('wss://localhost', sslOpts) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - c.end() - }) - - sslOpts2 = { - key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), - ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] - } - - it('should throw an error when it is called with cert and key set but no protocol specified', function () { - // to do rewrite wrap function - (function () { - var c = mqtt.connect(sslOpts2) - c.end() - }).should.throw('Missing secure protocol key') - }) - - it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { - (function () { - sslOpts2.protocol = 'UNKNOWNPROTOCOL' - var c = mqtt.connect(sslOpts2) - c.end() - }).should.throw() - }) - - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function () { - sslOpts2.protocol = 'mqtt' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function () { - sslOpts2.protocol = 'mqtts' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'mqtts') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function () { - sslOpts2.protocol = 'ws' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function () { - sslOpts2.protocol = 'wss' - var c = mqtt.connect(sslOpts2) - - c.options.should.have.property('protocol', 'wss') - - c.on('error', function () {}) - - c.should.be.instanceOf(mqtt.MqttClient) - }) - - it('should return an MqttClient with the clientid with option of clientId as empty string', function () { - var c = mqtt.connect('mqtt://localhost:1883', { - clientId: '' - }) - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '') - }) - - it('should return an MqttClient with the clientid with option of clientId empty', function () { - var c = mqtt.connect('mqtt://localhost:1883') - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId') - c.end() - }) - - it('should return an MqttClient with the clientid with option of with specific clientId', function () { - var c = mqtt.connect('mqtt://localhost:1883', { - clientId: '123' - }) - - c.should.be.instanceOf(mqtt.MqttClient) - c.options.should.have.property('clientId', '123') - c.end() - }) - }) -}) +'use strict' + +var fs = require('fs') +var path = require('path') +var mqtt = require('../') + +describe('mqtt', function () { + describe('#connect', function () { + var sslOpts, sslOpts2 + it('should return an MqttClient when connect is called with mqtt:/ url', function () { + var c = mqtt.connect('mqtt://localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should throw an error when called with no protocol specified', function () { + (function () { + var c = mqtt.connect('foo.bar.com') + c.end() + }).should.throw('Missing protocol') + }) + + it('should throw an error when called with no protocol specified - with options', function () { + (function () { + var c = mqtt.connect('tcp://foo.bar.com', { protocol: null }) + c.end() + }).should.throw('Missing protocol') + }) + + it('should return an MqttClient with username option set', function () { + var c = mqtt.connect('mqtt://user:pass@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.options.should.have.property('password', 'pass') + c.end() + }) + + it('should return an MqttClient with username and password options set', function () { + var c = mqtt.connect('mqtt://user@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('username', 'user') + c.end() + }) + + it('should return an MqttClient with the clientid with random value', function () { + var c = mqtt.connect('mqtt://user@localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end() + }) + + it('should return an MqttClient with the clientid with empty string', function () { + var c = mqtt.connect('mqtt://user@localhost:1883?clientId=') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + c.end() + }) + + it('should return an MqttClient with the clientid option set', function () { + var c = mqtt.connect('mqtt://user@localhost:1883?clientId=123') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end() + }) + + it('should return an MqttClient when connect is called with tcp:/ url', function () { + var c = mqtt.connect('tcp://localhost') + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient with correct host when called with a host and port', function () { + var c = mqtt.connect('tcp://user:pass@localhost:1883') + + c.options.should.have.property('hostname', 'localhost') + c.options.should.have.property('port', 1883) + c.end() + }) + + sslOpts = { + keyPath: path.join(__dirname, 'helpers', 'private-key.pem'), + certPath: path.join(__dirname, 'helpers', 'public-cert.pem'), + caPaths: [path.join(__dirname, 'helpers', 'public-cert.pem')] + } + + it('should return an MqttClient when connect is called with mqtts:/ url', function () { + var c = mqtt.connect('mqtts://localhost', sslOpts) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with ssl:/ url', function () { + var c = mqtt.connect('ssl://localhost', sslOpts) + + c.options.should.have.property('protocol', 'ssl') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with ws:/ url', function () { + var c = mqtt.connect('ws://localhost', sslOpts) + + c.options.should.have.property('protocol', 'ws') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + it('should return an MqttClient when connect is called with wss:/ url', function () { + var c = mqtt.connect('wss://localhost', sslOpts) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + c.end() + }) + + sslOpts2 = { + key: fs.readFileSync(path.join(__dirname, 'helpers', 'private-key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem')), + ca: [fs.readFileSync(path.join(__dirname, 'helpers', 'public-cert.pem'))] + } + + it('should throw an error when it is called with cert and key set but no protocol specified', function () { + // to do rewrite wrap function + (function () { + var c = mqtt.connect(sslOpts2) + c.end() + }).should.throw('Missing secure protocol key') + }) + + it('should throw an error when it is called with cert and key set and protocol other than allowed: mqtt,mqtts,ws,wss,wxs', function () { + (function () { + sslOpts2.protocol = 'UNKNOWNPROTOCOL' + var c = mqtt.connect(sslOpts2) + c.end() + }).should.throw() + }) + + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtt', function () { + sslOpts2.protocol = 'mqtt' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with mqtts set when connect is called key and cert set and protocol mqtts', function () { + sslOpts2.protocol = 'mqtts' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'mqtts') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with wss set when connect is called key and cert set and protocol ws', function () { + sslOpts2.protocol = 'ws' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return a MqttClient with wss set when connect is called key and cert set and protocol wss', function () { + sslOpts2.protocol = 'wss' + var c = mqtt.connect(sslOpts2) + + c.options.should.have.property('protocol', 'wss') + + c.on('error', function () {}) + + c.should.be.instanceOf(mqtt.MqttClient) + }) + + it('should return an MqttClient with the clientid with option of clientId as empty string', function () { + var c = mqtt.connect('mqtt://localhost:1883', { + clientId: '' + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '') + }) + + it('should return an MqttClient with the clientid with option of clientId empty', function () { + var c = mqtt.connect('mqtt://localhost:1883') + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId') + c.end() + }) + + it('should return an MqttClient with the clientid with option of with specific clientId', function () { + var c = mqtt.connect('mqtt://localhost:1883', { + clientId: '123' + }) + + c.should.be.instanceOf(mqtt.MqttClient) + c.options.should.have.property('clientId', '123') + c.end() + }) + }) +}) diff --git a/test/mqtt_store.js b/test/mqtt_store.js index 0eda04d8b..976a01aff 100644 --- a/test/mqtt_store.js +++ b/test/mqtt_store.js @@ -1,9 +1,9 @@ -'use strict' - -var mqtt = require('../lib/connect') - -describe('store in lib/connect/index.js (webpack entry point)', function () { - it('should create store', function (done) { - done(null, new mqtt.Store()) - }) -}) +'use strict' + +var mqtt = require('../lib/connect') + +describe('store in lib/connect/index.js (webpack entry point)', function () { + it('should create store', function (done) { + done(null, new mqtt.Store()) + }) +}) diff --git a/test/secure_client.js b/test/secure_client.js index 8c4904465..95b7a6197 100644 --- a/test/secure_client.js +++ b/test/secure_client.js @@ -1,188 +1,188 @@ -'use strict' - -var mqtt = require('..') -var path = require('path') -var abstractClientTests = require('./abstract_client') -var fs = require('fs') -var port = 9899 -var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') -var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') -var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') -var MqttSecureServer = require('./server').MqttSecureServer -var assert = require('chai').assert - -var serverListener = function (client) { - // this is the Server's MQTT Client - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({returnCode: 2}) - } else { - server.emit('connect', client) - client.connack({returnCode: 0}) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - /* jshint -W027 */ - /* eslint default-case:0 */ - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - /* jshint +W027 */ - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -var server = new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) -}, serverListener).listen(port) - -describe('MqttSecureClient', function () { - var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } - abstractClientTests(server, config) - - describe('with secure parameters', function () { - it('should validate successfully the CA', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI', function (done) { - var client = mqtt.connect('mqtts://localhost:' + port, { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate successfully the CA using URI with path', function (done) { - var client = mqtt.connect('mqtts://localhost:' + port + '/', { - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - done() - }) - }) - - it('should validate unsuccessfully the CA', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.once('error', function () { - done() - client.end() - client.on('error', function () {}) - }) - }) - - it('should emit close on TLS error', function (done) { - var client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(WRONG_CERT)], - rejectUnauthorized: true - }) - - client.on('error', function () {}) - - // TODO node v0.8.x emits multiple close events - client.once('close', function () { - done() - }) - }) - - it('should support SNI on the TLS connection', function (done) { - var hostname, client - server.removeAllListeners('secureConnection') // clear eventHandler - server.once('secureConnection', function (tlsSocket) { // one time eventHandler - assert.equal(tlsSocket.servername, hostname) // validate SNI set - server.setupConnection(tlsSocket) - }) - - hostname = 'localhost' - client = mqtt.connect({ - protocol: 'mqtts', - port: port, - ca: [fs.readFileSync(CERT)], - rejectUnauthorized: true, - host: hostname - }) - - client.on('error', function (err) { - done(err) - }) - - server.once('connect', function () { - server.on('secureConnection', server.setupConnection) // reset eventHandler - done() - }) - }) - }) -}) +'use strict' + +var mqtt = require('..') +var path = require('path') +var abstractClientTests = require('./abstract_client') +var fs = require('fs') +var port = 9899 +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') +var WRONG_CERT = path.join(__dirname, 'helpers', 'wrong-cert.pem') +var MqttSecureServer = require('./server').MqttSecureServer +var assert = require('chai').assert + +var serverListener = function (client) { + // this is the Server's MQTT Client + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({returnCode: 2}) + } else { + server.emit('connect', client) + client.connack({returnCode: 0}) + } + }) + + client.on('publish', function (packet) { + setImmediate(function () { + /* jshint -W027 */ + /* eslint default-case:0 */ + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + /* jshint +W027 */ + }) + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +var server = new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) +}, serverListener).listen(port) + +describe('MqttSecureClient', function () { + var config = { protocol: 'mqtts', port: port, rejectUnauthorized: false } + abstractClientTests(server, config) + + describe('with secure parameters', function () { + it('should validate successfully the CA', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate successfully the CA using URI', function (done) { + var client = mqtt.connect('mqtts://localhost:' + port, { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate successfully the CA using URI with path', function (done) { + var client = mqtt.connect('mqtts://localhost:' + port + '/', { + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + done() + }) + }) + + it('should validate unsuccessfully the CA', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true + }) + + client.once('error', function () { + done() + client.end() + client.on('error', function () {}) + }) + }) + + it('should emit close on TLS error', function (done) { + var client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(WRONG_CERT)], + rejectUnauthorized: true + }) + + client.on('error', function () {}) + + // TODO node v0.8.x emits multiple close events + client.once('close', function () { + done() + }) + }) + + it('should support SNI on the TLS connection', function (done) { + var hostname, client + server.removeAllListeners('secureConnection') // clear eventHandler + server.once('secureConnection', function (tlsSocket) { // one time eventHandler + assert.equal(tlsSocket.servername, hostname) // validate SNI set + server.setupConnection(tlsSocket) + }) + + hostname = 'localhost' + client = mqtt.connect({ + protocol: 'mqtts', + port: port, + ca: [fs.readFileSync(CERT)], + rejectUnauthorized: true, + host: hostname + }) + + client.on('error', function (err) { + done(err) + }) + + server.once('connect', function () { + server.on('secureConnection', server.setupConnection) // reset eventHandler + done() + }) + }) + }) +}) diff --git a/test/server.js b/test/server.js index 3b009d4fb..ccfe2f4d1 100644 --- a/test/server.js +++ b/test/server.js @@ -1,94 +1,94 @@ -'use strict' - -var net = require('net') -var tls = require('tls') -var Connection = require('mqtt-connection') - -/** - * MqttServer - * - * @param {Function} listener - fired on client connection - */ -class MqttServer extends net.Server { - constructor (listener) { - super() - this.connectionList = [] - - var that = this - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - }) - - if (listener) { - this.on('client', listener) - } - } -} - -/** - * MqttServerNoWait (w/o waiting for initialization) - * - * @param {Function} listener - fired on client connection - */ -class MqttServerNoWait extends net.Server { - constructor (listener) { - super() - this.connectionList = [] - - this.on('connection', function (duplex) { - this.connectionList.push(duplex) - var connection = new Connection(duplex) - // do not wait for connection to return to send it to the client. - this.emit('client', connection) - }) - - if (listener) { - this.on('client', listener) - } - } -} - -/** - * MqttSecureServer - * - * @param {Object} opts - server options - * @param {Function} listener - */ -class MqttSecureServer extends tls.Server { - constructor (opts, listener) { - if (typeof opts === 'function') { - listener = opts - opts = {} - } - - // sets a listener for the 'connection' event - super(opts) - this.connectionList = [] - - this.on('secureConnection', function (socket) { - this.connectionList.push(socket) - var that = this - var connection = new Connection(socket, function () { - that.emit('client', connection) - }) - }) - - if (listener) { - this.on('client', listener) - } - } - - setupConnection (duplex) { - var that = this - var connection = new Connection(duplex, function () { - that.emit('client', connection) - }) - } -} - -exports.MqttServer = MqttServer -exports.MqttServerNoWait = MqttServerNoWait -exports.MqttSecureServer = MqttSecureServer +'use strict' + +var net = require('net') +var tls = require('tls') +var Connection = require('mqtt-connection') + +/** + * MqttServer + * + * @param {Function} listener - fired on client connection + */ +class MqttServer extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + var that = this + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + }) + + if (listener) { + this.on('client', listener) + } + } +} + +/** + * MqttServerNoWait (w/o waiting for initialization) + * + * @param {Function} listener - fired on client connection + */ +class MqttServerNoWait extends net.Server { + constructor (listener) { + super() + this.connectionList = [] + + this.on('connection', function (duplex) { + this.connectionList.push(duplex) + var connection = new Connection(duplex) + // do not wait for connection to return to send it to the client. + this.emit('client', connection) + }) + + if (listener) { + this.on('client', listener) + } + } +} + +/** + * MqttSecureServer + * + * @param {Object} opts - server options + * @param {Function} listener + */ +class MqttSecureServer extends tls.Server { + constructor (opts, listener) { + if (typeof opts === 'function') { + listener = opts + opts = {} + } + + // sets a listener for the 'connection' event + super(opts) + this.connectionList = [] + + this.on('secureConnection', function (socket) { + this.connectionList.push(socket) + var that = this + var connection = new Connection(socket, function () { + that.emit('client', connection) + }) + }) + + if (listener) { + this.on('client', listener) + } + } + + setupConnection (duplex) { + var that = this + var connection = new Connection(duplex, function () { + that.emit('client', connection) + }) + } +} + +exports.MqttServer = MqttServer +exports.MqttServerNoWait = MqttServerNoWait +exports.MqttSecureServer = MqttSecureServer diff --git a/test/server_helpers_for_client_tests.js b/test/server_helpers_for_client_tests.js index e7ea345c4..9527d47e2 100644 --- a/test/server_helpers_for_client_tests.js +++ b/test/server_helpers_for_client_tests.js @@ -1,147 +1,147 @@ -'use strict' - -var MqttServer = require('./server').MqttServer -var MqttSecureServer = require('./server').MqttSecureServer -var debug = require('debug')('TEST:server_helpers') - -var path = require('path') -var fs = require('fs') -var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') -var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') - -var http = require('http') -var WebSocket = require('ws') -var MQTTConnection = require('mqtt-connection') - -/** - * This will build the client for the server to use during testing, and set up the - * server side client based on mqtt-connection for handling MQTT messages. - * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' - * @param {Function} handler - event handler - */ -function serverBuilder (protocol, handler) { - var defaultHandler = function (serverClient) { - serverClient.on('auth', function (packet) { - if (serverClient.writable) return false - var rc = 'reasonCode' - var connack = {} - connack[rc] = 0 - serverClient.connack(connack) - }) - serverClient.on('connect', function (packet) { - if (!serverClient.writable) return false - var rc = 'returnCode' - var connack = {} - if (serverClient.options && serverClient.options.protocolVersion === 5) { - rc = 'reasonCode' - if (packet.clientId === 'invalid') { - connack[rc] = 128 - } else { - connack[rc] = 0 - } - } else { - if (packet.clientId === 'invalid') { - connack[rc] = 2 - } else { - connack[rc] = 0 - } - } - if (packet.properties && packet.properties.authenticationMethod) { - return false - } else { - serverClient.connack(connack) - } - }) - - serverClient.on('publish', function (packet) { - if (!serverClient.writable) return false - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - serverClient.puback(packet) - break - case 2: - serverClient.pubrec(packet) - break - } - }) - }) - - serverClient.on('pubrel', function (packet) { - if (!serverClient.writable) return false - serverClient.pubcomp(packet) - }) - - serverClient.on('pubrec', function (packet) { - if (!serverClient.writable) return false - serverClient.pubrel(packet) - }) - - serverClient.on('pubcomp', function () { - // Nothing to be done - }) - - serverClient.on('subscribe', function (packet) { - if (!serverClient.writable) return false - serverClient.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - serverClient.on('unsubscribe', function (packet) { - if (!serverClient.writable) return false - packet.granted = packet.unsubscriptions.map(function () { return 0 }) - serverClient.unsuback(packet) - }) - - serverClient.on('pingreq', function () { - if (!serverClient.writable) return false - serverClient.pingresp() - }) - - serverClient.on('end', function () { - debug('disconnected from server') - }) - } - - if (!handler) { - handler = defaultHandler - } - - switch (protocol) { - case 'mqtt': - return new MqttServer(handler) - case 'mqtts': - return new MqttSecureServer({ - key: fs.readFileSync(KEY), - cert: fs.readFileSync(CERT) - }, - handler) - case 'ws': - var attachWebsocketServer = function (server) { - var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) - - webSocketServer.on('connection', function (ws) { - var stream = WebSocket.createWebSocketStream(ws) - var connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - server.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - connection.on('close', function () {}) - }) - } - - var httpServer = http.createServer() - attachWebsocketServer(httpServer) - httpServer.on('client', handler) - return httpServer - } -} - -exports.serverBuilder = serverBuilder +'use strict' + +var MqttServer = require('./server').MqttServer +var MqttSecureServer = require('./server').MqttSecureServer +var debug = require('debug')('TEST:server_helpers') + +var path = require('path') +var fs = require('fs') +var KEY = path.join(__dirname, 'helpers', 'tls-key.pem') +var CERT = path.join(__dirname, 'helpers', 'tls-cert.pem') + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') + +/** + * This will build the client for the server to use during testing, and set up the + * server side client based on mqtt-connection for handling MQTT messages. + * @param {String} protocol - 'mqtt', 'mqtts' or 'ws' + * @param {Function} handler - event handler + */ +function serverBuilder (protocol, handler) { + var defaultHandler = function (serverClient) { + serverClient.on('auth', function (packet) { + if (serverClient.writable) return false + var rc = 'reasonCode' + var connack = {} + connack[rc] = 0 + serverClient.connack(connack) + }) + serverClient.on('connect', function (packet) { + if (!serverClient.writable) return false + var rc = 'returnCode' + var connack = {} + if (serverClient.options && serverClient.options.protocolVersion === 5) { + rc = 'reasonCode' + if (packet.clientId === 'invalid') { + connack[rc] = 128 + } else { + connack[rc] = 0 + } + } else { + if (packet.clientId === 'invalid') { + connack[rc] = 2 + } else { + connack[rc] = 0 + } + } + if (packet.properties && packet.properties.authenticationMethod) { + return false + } else { + serverClient.connack(connack) + } + }) + + serverClient.on('publish', function (packet) { + if (!serverClient.writable) return false + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + serverClient.puback(packet) + break + case 2: + serverClient.pubrec(packet) + break + } + }) + }) + + serverClient.on('pubrel', function (packet) { + if (!serverClient.writable) return false + serverClient.pubcomp(packet) + }) + + serverClient.on('pubrec', function (packet) { + if (!serverClient.writable) return false + serverClient.pubrel(packet) + }) + + serverClient.on('pubcomp', function () { + // Nothing to be done + }) + + serverClient.on('subscribe', function (packet) { + if (!serverClient.writable) return false + serverClient.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + serverClient.on('unsubscribe', function (packet) { + if (!serverClient.writable) return false + packet.granted = packet.unsubscriptions.map(function () { return 0 }) + serverClient.unsuback(packet) + }) + + serverClient.on('pingreq', function () { + if (!serverClient.writable) return false + serverClient.pingresp() + }) + + serverClient.on('end', function () { + debug('disconnected from server') + }) + } + + if (!handler) { + handler = defaultHandler + } + + switch (protocol) { + case 'mqtt': + return new MqttServer(handler) + case 'mqtts': + return new MqttSecureServer({ + key: fs.readFileSync(KEY), + cert: fs.readFileSync(CERT) + }, + handler) + case 'ws': + var attachWebsocketServer = function (server) { + var webSocketServer = new WebSocket.Server({server: server, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + server.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + connection.on('close', function () {}) + }) + } + + var httpServer = http.createServer() + attachWebsocketServer(httpServer) + httpServer.on('client', handler) + return httpServer + } +} + +exports.serverBuilder = serverBuilder diff --git a/test/store.js b/test/store.js index 5244cdf84..1489b2138 100644 --- a/test/store.js +++ b/test/store.js @@ -1,10 +1,10 @@ -'use strict' - -var Store = require('../lib/store') -var abstractTest = require('../test/abstract_store') - -describe('in-memory store', function () { - abstractTest(function (done) { - done(null, new Store()) - }) -}) +'use strict' + +var Store = require('../lib/store') +var abstractTest = require('../test/abstract_store') + +describe('in-memory store', function () { + abstractTest(function (done) { + done(null, new Store()) + }) +}) diff --git a/test/unique_message_id_provider_client.js b/test/unique_message_id_provider_client.js index a23625a85..933d85b82 100644 --- a/test/unique_message_id_provider_client.js +++ b/test/unique_message_id_provider_client.js @@ -1,21 +1,21 @@ -'use strict' - -var abstractClientTests = require('./abstract_client') -var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder -var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') -var ports = require('./helpers/port_list') - -describe('UniqueMessageIdProviderMqttClient', function () { - var server = serverBuilder('mqtt') - var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} - server.listen(ports.PORTAND400) - - after(function () { - // clean up and make sure the server is no longer listening... - if (server.listening) { - server.close() - } - }) - - abstractClientTests(server, config) -}) +'use strict' + +var abstractClientTests = require('./abstract_client') +var serverBuilder = require('./server_helpers_for_client_tests').serverBuilder +var UniqueMessageIdProvider = require('../lib/unique-message-id-provider') +var ports = require('./helpers/port_list') + +describe('UniqueMessageIdProviderMqttClient', function () { + var server = serverBuilder('mqtt') + var config = {protocol: 'mqtt', port: ports.PORTAND400, messageIdProvider: new UniqueMessageIdProvider()} + server.listen(ports.PORTAND400) + + after(function () { + // clean up and make sure the server is no longer listening... + if (server.listening) { + server.close() + } + }) + + abstractClientTests(server, config) +}) diff --git a/test/util.js b/test/util.js index ab2661804..0dd559cb9 100644 --- a/test/util.js +++ b/test/util.js @@ -1,15 +1,15 @@ -'use strict' - -var Transform = require('readable-stream').Transform - -module.exports.testStream = function () { - return new Transform({ - transform (buf, enc, cb) { - var that = this - setImmediate(function () { - that.push(buf) - cb() - }) - } - }) -} +'use strict' + +var Transform = require('readable-stream').Transform + +module.exports.testStream = function () { + return new Transform({ + transform (buf, enc, cb) { + var that = this + setImmediate(function () { + that.push(buf) + cb() + }) + } + }) +} diff --git a/test/websocket_client.js b/test/websocket_client.js index 9eb7007c2..a7f59897a 100644 --- a/test/websocket_client.js +++ b/test/websocket_client.js @@ -1,191 +1,191 @@ -'use strict' - -var http = require('http') -var WebSocket = require('ws') -var MQTTConnection = require('mqtt-connection') -var abstractClientTests = require('./abstract_client') -var ports = require('./helpers/port_list') -var MqttServerNoWait = require('./server').MqttServerNoWait -var mqtt = require('../') -var xtend = require('xtend') -var assert = require('assert') -var port = 9999 -var httpServer = http.createServer() - -function attachWebsocketServer (httpServer) { - var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) - - webSocketServer.on('connection', function (ws) { - var stream = WebSocket.createWebSocketStream(ws) - var connection = new MQTTConnection(stream) - connection.protocol = ws.protocol - httpServer.emit('client', connection) - stream.on('error', function () {}) - connection.on('error', function () {}) - }) - - return httpServer -} - -function attachClientEventHandlers (client) { - client.on('connect', function (packet) { - if (packet.clientId === 'invalid') { - client.connack({ returnCode: 2 }) - } else { - httpServer.emit('connect', client) - client.connack({returnCode: 0}) - } - }) - - client.on('publish', function (packet) { - setImmediate(function () { - switch (packet.qos) { - case 0: - break - case 1: - client.puback(packet) - break - case 2: - client.pubrec(packet) - break - } - }) - }) - - client.on('pubrel', function (packet) { - client.pubcomp(packet) - }) - - client.on('pubrec', function (packet) { - client.pubrel(packet) - }) - - client.on('pubcomp', function () { - // Nothing to be done - }) - - client.on('subscribe', function (packet) { - client.suback({ - messageId: packet.messageId, - granted: packet.subscriptions.map(function (e) { - return e.qos - }) - }) - }) - - client.on('unsubscribe', function (packet) { - client.unsuback(packet) - }) - - client.on('pingreq', function () { - client.pingresp() - }) -} - -attachWebsocketServer(httpServer) - -httpServer.on('client', attachClientEventHandlers).listen(port) - -describe('Websocket Client', function () { - var baseConfig = { protocol: 'ws', port: port } - - function makeOptions (custom) { - // xtend returns a new object. Does not mutate arguments - return xtend(baseConfig, custom || {}) - } - - it('should use mqtt as the protocol by default', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqtt') - }) - mqtt.connect(makeOptions()).on('connect', function () { - this.end(true, done) - }) - }) - - it('should be able to transform the url (for e.g. to sign it)', function (done) { - var baseUrl = 'ws://localhost:9999/mqtt' - var sig = '?AUTH=token' - var expected = baseUrl + sig - var actual - var opts = makeOptions({ - path: '/mqtt', - transformWsUrl: function (url, opt, client) { - assert.equal(url, baseUrl) - assert.strictEqual(opt, opts) - assert.strictEqual(client.options, opts) - assert.strictEqual(typeof opt.transformWsUrl, 'function') - assert(client instanceof mqtt.MqttClient) - url += sig - actual = url - return url - }}) - mqtt.connect(opts) - .on('connect', function () { - assert.equal(this.stream.url, expected) - assert.equal(actual, expected) - this.end(true, done) - }) - }) - - it('should use mqttv3.1 as the protocol if using v3.1', function (done) { - httpServer.once('client', function (client) { - assert.strictEqual(client.protocol, 'mqttv3.1') - }) - - var opts = makeOptions({ - protocolId: 'MQIsdp', - protocolVersion: 3 - }) - - mqtt.connect(opts).on('connect', function () { - this.end(true, done) - }) - }) - - describe('reconnecting', () => { - it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { - var serverPort42Connected = false - var handler = function (serverClient) { - serverClient.on('connect', function (packet) { - serverClient.connack({returnCode: 0}) - }) - } - this.timeout(15000) - var actualURL41 = 'wss://localhost:9917/' - var actualURL42 = 'ws://localhost:9918/' - var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) - var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) - - serverPort42.on('listening', function () { - let client = mqtt.connect({ - protocol: 'wss', - servers: [ - { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, - { port: ports.PORTAND41, host: 'localhost' } - ], - keepalive: 50 - }) - serverPort41.once('client', function (c) { - assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') - assert(serverPort42Connected) - c.stream.destroy() - client.end(true, done) - serverPort41.close() - }) - serverPort42.once('client', function (c) { - serverPort42Connected = true - assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') - c.stream.destroy() - serverPort42.close() - }) - - client.once('connect', function () { - client.stream.destroy() - }) - }) - }) - }) - - abstractClientTests(httpServer, makeOptions()) -}) +'use strict' + +var http = require('http') +var WebSocket = require('ws') +var MQTTConnection = require('mqtt-connection') +var abstractClientTests = require('./abstract_client') +var ports = require('./helpers/port_list') +var MqttServerNoWait = require('./server').MqttServerNoWait +var mqtt = require('../') +var xtend = require('xtend') +var assert = require('assert') +var port = 9999 +var httpServer = http.createServer() + +function attachWebsocketServer (httpServer) { + var webSocketServer = new WebSocket.Server({server: httpServer, perMessageDeflate: false}) + + webSocketServer.on('connection', function (ws) { + var stream = WebSocket.createWebSocketStream(ws) + var connection = new MQTTConnection(stream) + connection.protocol = ws.protocol + httpServer.emit('client', connection) + stream.on('error', function () {}) + connection.on('error', function () {}) + }) + + return httpServer +} + +function attachClientEventHandlers (client) { + client.on('connect', function (packet) { + if (packet.clientId === 'invalid') { + client.connack({ returnCode: 2 }) + } else { + httpServer.emit('connect', client) + client.connack({returnCode: 0}) + } + }) + + client.on('publish', function (packet) { + setImmediate(function () { + switch (packet.qos) { + case 0: + break + case 1: + client.puback(packet) + break + case 2: + client.pubrec(packet) + break + } + }) + }) + + client.on('pubrel', function (packet) { + client.pubcomp(packet) + }) + + client.on('pubrec', function (packet) { + client.pubrel(packet) + }) + + client.on('pubcomp', function () { + // Nothing to be done + }) + + client.on('subscribe', function (packet) { + client.suback({ + messageId: packet.messageId, + granted: packet.subscriptions.map(function (e) { + return e.qos + }) + }) + }) + + client.on('unsubscribe', function (packet) { + client.unsuback(packet) + }) + + client.on('pingreq', function () { + client.pingresp() + }) +} + +attachWebsocketServer(httpServer) + +httpServer.on('client', attachClientEventHandlers).listen(port) + +describe('Websocket Client', function () { + var baseConfig = { protocol: 'ws', port: port } + + function makeOptions (custom) { + // xtend returns a new object. Does not mutate arguments + return xtend(baseConfig, custom || {}) + } + + it('should use mqtt as the protocol by default', function (done) { + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqtt') + }) + mqtt.connect(makeOptions()).on('connect', function () { + this.end(true, done) + }) + }) + + it('should be able to transform the url (for e.g. to sign it)', function (done) { + var baseUrl = 'ws://localhost:9999/mqtt' + var sig = '?AUTH=token' + var expected = baseUrl + sig + var actual + var opts = makeOptions({ + path: '/mqtt', + transformWsUrl: function (url, opt, client) { + assert.equal(url, baseUrl) + assert.strictEqual(opt, opts) + assert.strictEqual(client.options, opts) + assert.strictEqual(typeof opt.transformWsUrl, 'function') + assert(client instanceof mqtt.MqttClient) + url += sig + actual = url + return url + }}) + mqtt.connect(opts) + .on('connect', function () { + assert.equal(this.stream.url, expected) + assert.equal(actual, expected) + this.end(true, done) + }) + }) + + it('should use mqttv3.1 as the protocol if using v3.1', function (done) { + httpServer.once('client', function (client) { + assert.strictEqual(client.protocol, 'mqttv3.1') + }) + + var opts = makeOptions({ + protocolId: 'MQIsdp', + protocolVersion: 3 + }) + + mqtt.connect(opts).on('connect', function () { + this.end(true, done) + }) + }) + + describe('reconnecting', () => { + it('should reconnect to multiple host-ports-protocol combinations if servers is passed', function (done) { + var serverPort42Connected = false + var handler = function (serverClient) { + serverClient.on('connect', function (packet) { + serverClient.connack({returnCode: 0}) + }) + } + this.timeout(15000) + var actualURL41 = 'wss://localhost:9917/' + var actualURL42 = 'ws://localhost:9918/' + var serverPort41 = new MqttServerNoWait(handler).listen(ports.PORTAND41) + var serverPort42 = new MqttServerNoWait(handler).listen(ports.PORTAND42) + + serverPort42.on('listening', function () { + let client = mqtt.connect({ + protocol: 'wss', + servers: [ + { port: ports.PORTAND42, host: 'localhost', protocol: 'ws' }, + { port: ports.PORTAND41, host: 'localhost' } + ], + keepalive: 50 + }) + serverPort41.once('client', function (c) { + assert.equal(client.stream.url, actualURL41, 'Protocol for second client should use the default protocol: wss, on port: port + 41.') + assert(serverPort42Connected) + c.stream.destroy() + client.end(true, done) + serverPort41.close() + }) + serverPort42.once('client', function (c) { + serverPort42Connected = true + assert.equal(client.stream.url, actualURL42, 'Protocol for connection should use ws, on port: port + 42.') + c.stream.destroy() + serverPort42.close() + }) + + client.once('connect', function () { + client.stream.destroy() + }) + }) + }) + }) + + abstractClientTests(httpServer, makeOptions()) +}) diff --git a/types/lib/client-options.d.ts b/types/lib/client-options.d.ts index 0e76c4fd3..a8cf962d6 100644 --- a/types/lib/client-options.d.ts +++ b/types/lib/client-options.d.ts @@ -149,7 +149,7 @@ export interface IClientPublishOptions { * MQTT 5.0 properties object */ properties?: { - payloadFormatIndicator?: boolean, + payloadFormatIndicator?: number, messageExpiryInterval?: number, topicAlias?: string, responseTopic?: string,