├── .gitignore ├── .gitmodules ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── benchmark.sh ├── benchmark ├── receive.js └── send.js ├── example.js ├── index.js ├── mqtt-stack.png ├── package.json ├── src ├── client.js ├── middlewares │ ├── authentication.js │ ├── authorization.js │ ├── connection.js │ ├── inbound_manager.js │ ├── keep_alive.js │ ├── last_will.js │ ├── memory_backend.js │ ├── outbound_manager.js │ ├── packet_emitter.js │ ├── retain_manager.js │ ├── session_manager.js │ └── subscription_manager.js ├── stack.js └── utils │ ├── middleware.js │ └── timer.js └── test ├── spec_test.js ├── test_broker.js └── unit ├── index.js ├── middlewares ├── authentication.js ├── authorization.js ├── connection.js ├── inbound_manager.js ├── keep_alive.js ├── last_will.js ├── outbound_manager.js ├── packet_emitter.js ├── retain_manager.js ├── session_manager.js └── subscription_manager.js ├── stack.js ├── stack_helper.js └── utils └── timer.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | .idea -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec"] 2 | path = spec 3 | url = git://github.com/mqttjs/mqtt-spec.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | before_install: 5 | - npm install npm@latest -g 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # mqtt-stack is an OPEN Open Source Project 2 | 3 | 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. 4 | 5 | ## Rules 6 | 7 | There are a few basic ground-rules for contributors: 8 | 9 | 1. **No `--force` pushes** or modifying the Git history in any way. 10 | 1. **Non-master branches** ought to be used for ongoing work. 11 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 12 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 13 | 1. Contributors should attempt to adhere to the prevailing code-style. 14 | 15 | ## Releases 16 | 17 | Declaring formal releases remains the prerogative of the project maintainer. 18 | 19 | ## Changes to this arrangement 20 | 21 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | ## Copyright (c) 2014-2015 mqtt-stack contributors 4 | 5 | *mqtt-stack contributors listed at * 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![mqtt-stack](https://raw.githubusercontent.com/kokeksibir/mqtt-stack/master/mqtt-stack.png) 2 | 3 | [![Build Status](https://travis-ci.org/mqttjs/mqtt-stack.png)](https://travis-ci.org/mqttjs/mqtt-stack) [![npm version](https://badge.fury.io/js/mqtt-stack.svg)](http://badge.fury.io/js/mqtt-stack) 4 | 5 | **middleware based components to build a custom mqtt broker** 6 | 7 | *In development, not yet stable.* 8 | 9 | ## Usage 10 | 11 | mqtt-stack is available in npm, first you need to install 12 | 13 | ```bash 14 | npm install mqtt-stack --save 15 | ``` 16 | 17 | require it in your project 18 | 19 | ```js 20 | var mqttStack = require('mqtt-stack'); 21 | ``` 22 | 23 | instantiate stack 24 | 25 | ```js 26 | var stack = new mqttStack.Stack(); 27 | ``` 28 | 29 | register any middleware to be used by calling `use` method 30 | 31 | ```js 32 | stack.use(new mqttStack.MemoryBackend()); 33 | ``` 34 | 35 | ## Middlewares 36 | In a broad terms, mqtt-stack middlewares are components that listen mqtt connection stream to perform actions according to their specific broker functionality. 37 | 38 | ### Interface 39 | `Middleware` base class implementation is available in module exports, developpers are encouraged to inherit from that base class. mqtt-stack middlewares may implement following interface methods. 40 | 41 | #### constructor (config) 42 | Standard object constructor function takes configuration object as argument. If middleware is inherited from `Middleware` base class `super(config, defaults)` call sets up `config` member attribute of middleware object. 43 | 44 | ```js 45 | class MyMiddleware { 46 | constructor(config) { 47 | let defaults = {some: 'value'}; 48 | /* calling super function sets this.config and 49 | * this.name attributes */ 50 | super(config, defaults); 51 | } 52 | } 53 | ``` 54 | #### install (client) 55 | Method is called once a new connection is established. 56 | 57 | #### handle (client, packet, next, done) 58 | Method is called once a packet is received. Once middleware finishes its action, it should either call `next` function to propagate to next middleware or call `done` function to terminate propagation. 59 | 60 | #### callback handlers (ctx, store, next, done) 61 | Other than these interface methods, middleware may handle a stack `callback` by exposing a method function with callback name. For instance, please check OutboundManager middleware (path: `src/middlewares/outbound_manager.js`) to see `forwardMessage` callback handler. `ctx` argument is an object which contains any relevant data required for callback handling. `store` is an output argument, that is updated by callback handlers. `done` terminates callback chain and returns callback. 62 | 63 | ### Built-in Middlewares 64 | mqtt-stack provide some built-in middlewares to provide basic MQTT Broker functionality. Keep in mind that those middlewares are not mandatory, on contrary they are designed to be easily replacible. 65 | 66 | #### Authentication 67 | Simple authentication binder middleware that executes `authenticateConnection` callback handler with `{client, packet, username, password}` context if client is not authenticated. 68 | 69 | #### Authorize 70 | Simple authorization binder middleware that executes `authorizePacket` callback handler with `{client, packet}` context for every received packet. 71 | 72 | #### Connection 73 | Simple connection management middleware. It observes connection status. 74 | 75 | When connection is closed gracefully it executes `cleanDisconnect` callback handler with `{client}` context. 76 | When connection is closed unexpectedly it executes `uncleanDisconnect` callback handler with `{client}` context. 77 | 78 | It exposes `closeClient` callback handler that will terminate client connection. 79 | 80 | #### InboundManager 81 | Handles client's `PUBLISH` command by executing `relayMessage` callback handler with `{client, packet, topic, payload}` context. Once callback handler finishes and it sends `PUBACK` message to client if its QoS is 1. 82 | 83 | #### KeepAlive 84 | Manages client connection's life span. Once client's `CONNECT` command is received, if it contains `keepalive` duration, middleware bounds life time of connection with this duration and resets the time on every received packet. It executes `closeClient` callback handler with `{client}` context if no packet is received within `keepalive` time frame. 85 | 86 | It also responds received `PINGREQ` commands with `PINGRESP`. 87 | 88 | #### LastWill 89 | Sends `last will` packet if client is disconnected unexpectedly. Once client's `CONNECT` command is received if it contains `last will` packet, will packet is stored. This middleware exposes `uncleanDisconnect` callback handler that sends will packet. 90 | 91 | #### MemoryBackend 92 | Simple non-persistent backend storage middleware. It stores clients' subscription list and topics' retained messages in memory. It exposes following callback handlers 93 | 94 | * `storeSubscription` stores that `ctx.client` is subscribed to `ctx.topic` with `ctx.qos` QoS level. 95 | * `removeSubscription` removes subscription record of `ctx.client` for topic `ctx.topic`. 96 | * `clearSubscriptions` removes all stored subscription data for `ctx.client`. 97 | * `lookupSubscriptions` returns all stored subscription data for `ctx.client` in `store` argument. 98 | * `storeRetainedMessage` clears previous retained message of `ctx.topic` and if `ctx.packet.payload` is not empty stores `ctx.packet` as new retained message. 99 | * `lookupRetainedMessages` returns stored retained message of `ctx.topic` in `store` argument. 100 | * `relayMessage` relays `ctx.packet` to subscribers of `ctx.packet.topic` by executing `forwardMessage` callback handler with context `{client, packet}`. 101 | * `subscribeTopic` subscribes `ctx.client` to `ctx.topic` with QoS level defined by `ctx.qos`. 102 | * `unsubscribeTopic` unsubscribes `ctx.client` from `ctx.topic`. 103 | * `storeOfflineMessage` stores `ctx.packet` for offline `ctx.client` with `ctx.messageId`. 104 | * `lookupOfflineMessages` returns all messages stored for `ctx.client` in `store` argument. 105 | * `removeOfflineMessages` removes messages with id's in the list `ctx.messageIds` stored for client with id `ctx.clientId`. 106 | * `clearOfflineMessages` removes all messages stored for client with id `ctx.clientId`. 107 | 108 | #### OutboundManager 109 | Manages outgoing messages. Handles client's `PUBACK` command. Exposes `forwardMessage` that publishes message to client. 110 | 111 | #### PacketEmitter 112 | Simple event bridge that establishes connection with an eventemitter and connection. Event emitter should be set by calling `setClientHandler` method before it is used. 113 | 114 | #### RetainManager 115 | Manages retained messages for topics. If client's `PUBLISH` command has flag `retain` it executes `storeRetainedMessage` callback handler with `{client, topic of packet, packet}` context. 116 | 117 | It exposes `subscribeTopic` callback handler that first executes `lookupRetainedMessages` callback handler with `{topic}` then if topic has retained message executes `forwardMessage` handler with `{client, retained packet}` 118 | 119 | #### SessionManager 120 | Manages the clients session and calls callbacks to manage the stored subscriptions for clean and unclean clients. Once client's `CONNECT` command is received, 121 | * if it contains `clean` flag, session manager does not store its subscriptions for later connections and also executes `clearSubscriptions` callback handler with `{client, packet, clientId}` context to destroy clients previous session. then sends `CONNACK` to client. 122 | * it it `clean` flag is false or not exists, session manager first executes `lookupSubscriptions` callback handler with `{client, packet, clientId}` context to retrieve old subscription list, then executes `subscribeTopic` callback handler for each subscription in list with `{client, packet, topic, subscription QoS}` context to restore old subscriptions. After session is restored, `CONNACK` is sent to client. 123 | 124 | #### SubscriptionManager 125 | Manages client's `SUBSCRIBE` and `UNSUBSCRIBE` commands. For subscibe, it executes `subscribeTopic` callback handler with `{client, packet, topic, QoS level}` context, then `SUBACK` is sent to client. For unsubscribe it executes `unsubscribeTopic` callback handler with `{client, packet, topic}` context, then `UNSUBACK` is sent to client. 126 | 127 | ## Tests 128 | Unit test are available in test folder. Project includes `mqtt-spec` as git submodule. Stack is tested for mqtt specifications using `mqtt-spec` module. Very primitive benchmarking results +20k message per second for loopback network. 129 | 130 | ## Contributing 131 | 132 | mqtt-stack is an **OPEN Open Source Project**. This means that: 133 | 134 | > 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. 135 | 136 | See the [CONTRIBUTING.md](https://github.com/mqttjs/mqtt-stack/blob/master/CONTRIBUTING.md) file for more details. 137 | 138 | ### Contributors 139 | 140 | mqtt-stack is only possible due to the excellent work of the following contributors: 141 | 142 | 143 | 144 | 145 | 146 |
Joël GähwilerGitHub/256dpiTwitter/@256dpi
Matteo CollinaGitHub/mcollinaTwitter/@matteocollina
M Kamil SulubulutGitHub/kokeksibirTwitter/@kokeksibir
147 | 148 | ### License 149 | 150 | MIT 151 | -------------------------------------------------------------------------------- /benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PORT=5000 4 | 5 | node example.js & 6 | sleep 1 7 | node benchmark/receive.js & 8 | sleep 1 9 | node benchmark/send.js 10 | -------------------------------------------------------------------------------- /benchmark/receive.js: -------------------------------------------------------------------------------- 1 | var mqtt = require('mqtt'); 2 | 3 | var port = process.env['PORT'] || 1883; 4 | 5 | var counter = 0; 6 | var interval = 5000; 7 | 8 | function count() { 9 | console.log('[recv]', Math.round(counter / interval * 1000), 'msg/s'); 10 | counter = 0; 11 | } 12 | 13 | setInterval(count, interval); 14 | 15 | var client = mqtt.connect({ 16 | port: port, 17 | host: 'localhost', 18 | clean: true, 19 | keepalive: 0, 20 | encoding: 'binary' 21 | }); 22 | 23 | client.on('connect', function() { 24 | console.log('[recv] connected to', port); 25 | 26 | client.subscribe('test', function(){ 27 | console.log('[recv] subscribed'); 28 | }); 29 | 30 | client.on('message', function() { 31 | counter++; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /benchmark/send.js: -------------------------------------------------------------------------------- 1 | var mqtt = require('mqtt'); 2 | 3 | var port = process.env['PORT'] || 1883; 4 | 5 | var counter = 0; 6 | var interval = 5000; 7 | 8 | function count() { 9 | console.log('[send]', Math.round(counter / interval * 1000), 'msg/s'); 10 | counter = 0; 11 | } 12 | 13 | setInterval(count, interval); 14 | 15 | var client = mqtt.connect({ 16 | port: port, 17 | host: 'localhost', 18 | clean: true, 19 | keepalive: 0, 20 | encoding: 'binary' 21 | }); 22 | 23 | function immediatePublish() { 24 | setImmediate(publish); 25 | } 26 | 27 | function publish() { 28 | counter++; 29 | client.publish('test', counter.toString(), immediatePublish); 30 | } 31 | 32 | client.on('connect', function(){ 33 | console.log('[send] connected to', port); 34 | publish(); 35 | }); 36 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var net = require('net'), 2 | mqttStack = require('./index'); 3 | 4 | var stack = new mqttStack.Stack(); 5 | 6 | stack.use(new mqttStack.MemoryBackend()); 7 | stack.use(new mqttStack.Connection()); 8 | stack.use(new mqttStack.KeepAlive()); 9 | stack.use(new mqttStack.LastWill()); 10 | stack.use(new mqttStack.SessionManager()); 11 | stack.use(new mqttStack.RetainManager()); 12 | stack.use(new mqttStack.InboundManager()); 13 | stack.use(new mqttStack.OutboundManager()); 14 | stack.use(new mqttStack.SubscriptionManager()); 15 | 16 | var port = process.env['PORT'] || 1883; 17 | 18 | var server = net.createServer(stack.handler()); 19 | 20 | server.listen(port, function(){ 21 | console.log('[serv] listening on', port) 22 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Stack = require('./src/stack'); 2 | module.exports.Middleware = require('./src/utils/middleware'); 3 | module.exports.KeepAlive = require('./src/middlewares/keep_alive'); 4 | module.exports.LastWill = require('./src/middlewares/last_will'); 5 | module.exports.Authentication = require('./src/middlewares/authentication'); 6 | module.exports.Authorization = require('./src/middlewares/authorization'); 7 | module.exports.Connection = require('./src/middlewares/connection'); 8 | module.exports.MemoryBackend = require('./src/middlewares/memory_backend'); 9 | module.exports.PacketEmitter = require('./src/middlewares/packet_emitter'); 10 | module.exports.SessionManager = require('./src/middlewares/session_manager'); 11 | module.exports.RetainManager = require('./src/middlewares/retain_manager'); 12 | module.exports.SubscriptionManager = require('./src/middlewares/subscription_manager'); 13 | module.exports.InboundManager = require('./src/middlewares/inbound_manager'); 14 | module.exports.OutboundManager = require('./src/middlewares/outbound_manager'); 15 | -------------------------------------------------------------------------------- /mqtt-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mqttjs/mqtt-stack/408e362b56ea2a6306afaa0ffce174049bf68849/mqtt-stack.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-stack", 3 | "version": "0.0.3", 4 | "engines": { 5 | "node": ">=4.00" 6 | }, 7 | "description": "middleware based components to build a custom mqtt broker", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "./node_modules/.bin/mocha --recursive --reporter list" 11 | }, 12 | "contributors": [ 13 | "Joël Gähwiler (https://github.com/256dpi)", 14 | "Matteo Collina (https://github.com/mcollina)", 15 | "Kamil Sulubulut (https://github.com/kokeksibir)" 16 | ], 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "github.com/mqttjs/mqtt-stack" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/mqttjs/mqtt-stack/issues" 24 | }, 25 | "devDependencies": { 26 | "expect.js": "0.3.1", 27 | "mocha": "2.2.1", 28 | "mqtt": "^1.6.0", 29 | "mqtt-connection": "2.1.1" 30 | }, 31 | "dependencies": { 32 | "async": "1.4.2", 33 | "mqtt-packet": "^4.0.3", 34 | "qlobber": "^0.5.3", 35 | "underscore": "1.8.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let mqtt = require('mqtt-packet'); 3 | let EventEmitter = require('events').EventEmitter; 4 | 5 | /** 6 | * Client Class 7 | * 8 | * Represents a connected client. 9 | */ 10 | class Client extends EventEmitter { 11 | /** 12 | * constructor 13 | * 14 | * @param stack 15 | * @param stream 16 | * @constructor 17 | */ 18 | constructor(stack, stream) { 19 | super(); 20 | let self = this; 21 | 22 | this.stack = stack; 23 | this.stream = stream; 24 | this._parser = mqtt.parser(); 25 | this._workload = 1; 26 | this._dead = false; 27 | 28 | this.stack.install(this); 29 | 30 | stream.on('readable', self._work.bind(self)); 31 | stream.on('error', this.emit.bind(this, 'error')); 32 | stream.on('close', this.emit.bind(this, 'close')); 33 | 34 | 35 | 36 | this._parser.on('packet', function (packet) { 37 | self._workload++; 38 | stack.process(self, packet, self._work.bind(self)); 39 | }); 40 | 41 | this._parser.on('error', this.emit.bind(this, 'error')); 42 | 43 | this._work(); 44 | } 45 | 46 | /** 47 | * Work on incomming packets. 48 | * 49 | * @private 50 | */ 51 | _work() { 52 | this._workload--; 53 | 54 | if (this._workload <= 0) { 55 | this._workload = 0; 56 | let chunk = this.stream.read(); 57 | 58 | if (chunk) { 59 | this._parser.parse(chunk); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Write data to the clients stream. 66 | * 67 | * @param packet 68 | * @param done 69 | */ 70 | write(packet, done) { 71 | if (!this._dead) { 72 | if(mqtt.writeToStream(packet, this.stream)) { 73 | setImmediate(done); 74 | } 75 | else { 76 | this.stream.once('drain', done); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Close the connection 83 | * 84 | * @param done 85 | */ 86 | close(done) { 87 | this._dead = true; 88 | 89 | if (this.stream.destroy) { 90 | this.stream.destroy(done); 91 | } else { 92 | this.stream.end(done); 93 | } 94 | } 95 | } 96 | 97 | module.exports = Client; 98 | -------------------------------------------------------------------------------- /src/middlewares/authentication.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | /** 4 | * Authentication Middleware 5 | * 6 | * Manges connection level authentication. 7 | * 8 | * Required callbacks: 9 | * - authenticateConnection 10 | */ 11 | 12 | class Authentication extends Middleware { 13 | /** 14 | * Flags all clients as not authenticated first. 15 | * @param client 16 | */ 17 | install(client) { 18 | client._authenticated = false; 19 | } 20 | 21 | /** 22 | * Executes 'authenticateConnection' for every new 'connect'. Sends 23 | * 'connack' with 'returnCode: 4' and dismisses packet if authentication 24 | * fails 25 | * 26 | * @param client 27 | * @param packet 28 | * @param next 29 | * @param done 30 | */ 31 | handle(client, packet, next, done) { 32 | if (packet.cmd == 'connect') { 33 | if (!client._authenticated) { 34 | let store = {}; 35 | this.stack.execute('authenticateConnection', { 36 | client: client, 37 | packet: packet, 38 | username: packet.username, 39 | password: packet.password 40 | }, store, function (err) { 41 | if (err) return next(err); 42 | 43 | if (store.valid) { 44 | client._authenticated = true; 45 | return next(); 46 | } else { 47 | return client.write({ 48 | cmd: 'connack', 49 | returnCode: 4 50 | }, done); 51 | } 52 | }); 53 | } 54 | } else { 55 | return next(); 56 | } 57 | } 58 | } 59 | 60 | module.exports = Authentication; 61 | -------------------------------------------------------------------------------- /src/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | 4 | /** 5 | * Authorization Middleware 6 | * 7 | * Authorizes invidual packet types. 8 | * 9 | * Required callbacks: 10 | * - authorizePacket 11 | */ 12 | 13 | class Authorization extends Middleware { 14 | /** 15 | * Executes 'authorizePacket' for every packet and only calls 16 | * propagates if authorization is valid. 17 | * 18 | * @param client 19 | * @param packet 20 | * @param next 21 | * @param done 22 | */ 23 | handle(client, packet, next, done) { 24 | let store = {}; 25 | this.stack.execute('authorizePacket', { 26 | client: client, 27 | packet: packet 28 | }, store, function (err) { 29 | if (err) return next(err); 30 | 31 | if (store.valid) { 32 | return next(); 33 | } else { 34 | return done(); 35 | } 36 | }); 37 | } 38 | } 39 | 40 | module.exports = Authorization; 41 | -------------------------------------------------------------------------------- /src/middlewares/connection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | let crypto = require('crypto'); 4 | 5 | /** 6 | * Connection Middleware 7 | * 8 | * Manages the basic connection. 9 | * 10 | * Exposed callbacks: 11 | * - closeClient 12 | * Required callbacks: 13 | * - uncleanDisconnect 14 | * - cleanDisconnect 15 | */ 16 | class Connection extends Middleware { 17 | /** 18 | * constructor 19 | * 20 | * @param config.forceMQTT4 - enable to force newest protocol 21 | */ 22 | constructor(config) { 23 | let defaults = { 24 | forceMQTT4: false 25 | }; 26 | super(config, defaults); 27 | } 28 | 29 | /** 30 | * Will execute 'uncleanDisconnect' on 'close' and 'error' 31 | * 32 | * - closes client on 'error' and executes 'uncleanDisconnect' 33 | * - executes 'uncleanDisconnect' on 'close' without a previous 'disconnect' packet 34 | * 35 | * @param client 36 | */ 37 | install(client) { 38 | let self = this; 39 | 40 | client._sent_first = false; 41 | client._sent_disconnect = false; 42 | 43 | client.on('error', function () { 44 | if (!client._sent_disconnect) { 45 | client.close(); 46 | return self.stack.execute('uncleanDisconnect', { 47 | client: client 48 | }); 49 | } 50 | }); 51 | 52 | client.on('close', function () { 53 | if (!client._sent_disconnect) { 54 | return self.stack.execute('uncleanDisconnect', { 55 | client: client 56 | }); 57 | } 58 | }); 59 | } 60 | 61 | /** 62 | * Handles 'connect' and 'disconnect' packets. 63 | * 64 | * - closes client if first packet is not a 'connect' packet 65 | * - closes client if clientID is empty and clean = false 66 | * - closes client and executes 'uncleanDisconnect' if connect gets received more than once 67 | * - closes client on 'disconnect' and executes 'cleanDisconnect' 68 | * - assigns a unique client_id when id is missing 69 | * - forces proper mqtt protocol version and Id if enabled (forceMQTT4) 70 | * 71 | * @param client 72 | * @param packet 73 | * @param next 74 | * @param done 75 | */ 76 | handle(client, packet, next, done) { 77 | let self = this; 78 | if (!client._sent_first) { 79 | client._sent_first = true; 80 | if (packet.cmd == 'connect') { 81 | if (this.config.forceMQTT4 && (packet.protocolVersion !== 4 || packet.protocolId !== 'MQTT')) { 82 | client.close(); 83 | return done(); 84 | } else if ((!packet.clientId || packet.clientId.length === 0) && packet.clean === false) { 85 | client.close(); 86 | return done(); 87 | } else { 88 | if (!packet.clientId || packet.clientId.length === 0) { 89 | packet.clientId = crypto.randomBytes(16).toString('hex'); 90 | } 91 | return next(); 92 | } 93 | } else { 94 | client.close(); 95 | return done(); 96 | } 97 | } else { 98 | if (packet.cmd == 'connect') { 99 | client.close(); 100 | return self.stack.execute('uncleanDisconnect', { 101 | client: client 102 | }, done); 103 | } else if (packet.cmd == 'disconnect') { 104 | client._sent_disconnect = true; 105 | client.close(); 106 | return self.stack.execute('cleanDisconnect', { 107 | client: client 108 | }, done); 109 | } else { 110 | return next(); 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * Execute to immediately close the client and execute 117 | * 'uncleanDisconnect' right after. 118 | * 119 | * @param ctx 120 | * @param __ 121 | * @param callback 122 | */ 123 | closeClient(ctx, __, callback) { 124 | let self = this; 125 | self.stack.execute('uncleanDisconnect', ctx, callback); 126 | return ctx.client.close(); 127 | } 128 | } 129 | module.exports = Connection; 130 | -------------------------------------------------------------------------------- /src/middlewares/inbound_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | 4 | /** 5 | * InboundManager Middleware 6 | * 7 | * Manages incomming publish packets. 8 | * Required callbacks: 9 | * - relayMessage 10 | */ 11 | class InboundManager extends Middleware { 12 | /** 13 | * Handles 'publish' messages and executes 'relayMessage'. Sends 14 | * 'puback' for QoS1 messages. 15 | * 16 | * @param client 17 | * @param packet 18 | * @param next 19 | * @param done 20 | */ 21 | handle(client, packet, next, done) { 22 | let self = this; 23 | if (packet.cmd == 'publish') { 24 | self.stack.execute('relayMessage', { 25 | client: client, 26 | packet: packet, 27 | topic: packet.topic, 28 | payload: packet.payload 29 | }, function (err) { 30 | if (err) return next(err); 31 | if (packet.qos == 1) { 32 | return client.write({ 33 | cmd: 'puback', 34 | messageId: packet.messageId 35 | }, done); 36 | } else { 37 | return done(); 38 | } 39 | }); 40 | } else { 41 | return next(); 42 | } 43 | } 44 | } 45 | 46 | module.exports = InboundManager; 47 | -------------------------------------------------------------------------------- /src/middlewares/keep_alive.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | let Timer = require('../utils/timer'); 4 | 5 | /** 6 | * KeepAlive Middleware 7 | * 8 | * TODO: add min and max values for client keepalive? 9 | * TODO: flag to discard 0 keepalive? 10 | * 11 | * Required callbacks: 12 | * - closeClient 13 | * 14 | */ 15 | class KeepAlive extends Middleware { 16 | /** 17 | * constructor 18 | * 19 | * @param config.defaultTimeout - the default timeout 20 | * @param config.grace - the grace to be allowed 21 | */ 22 | constructor(config) { 23 | let defaults = { 24 | defaultTimeout: false, 25 | grace: 2 26 | }; 27 | super(config, defaults); 28 | } 29 | 30 | /** 31 | * Starts the default timer and executes 'closeClient' on no activity. 32 | * 33 | * @param client 34 | */ 35 | install(client) { 36 | let self = this; 37 | if (this.config.defaultTimeout) { 38 | client._keep_alive_timer = new Timer(this.config.defaultTimeout * 1000, function () { 39 | return self.stack.execute('closeClient', { 40 | client: client 41 | }); 42 | }); 43 | } 44 | } 45 | 46 | /** 47 | * Starts timer with settings read from 'connect' packet, 48 | * resets timer on any client activity, handles and 49 | * dismisses 'pingreq' packets, executes 'closeClient' 50 | * if client misses a ping 51 | * 52 | * @param client 53 | * @param packet 54 | * @param next 55 | * @param done 56 | */ 57 | handle(client, packet, next, done) { 58 | let self = this; 59 | if (packet.cmd == 'connect') { 60 | if (client._keep_alive_timer) { 61 | client._keep_alive_timer.clear(); 62 | } 63 | 64 | if (packet.keepalive > 0) { 65 | let timeout = packet.keepalive * 1000 * this.config.grace; 66 | 67 | client._keep_alive_timer = new Timer(timeout, function () { 68 | self.stack.execute('closeClient', { 69 | client: client 70 | }); 71 | }); 72 | } 73 | 74 | return next(); 75 | } else { 76 | if (client._keep_alive_timer) { 77 | client._keep_alive_timer.reset(); 78 | } 79 | 80 | if (packet.cmd == 'pingreq') { 81 | return client.write({ 82 | cmd: 'pingresp' 83 | }, done); 84 | } else { 85 | return next(); 86 | } 87 | } 88 | } 89 | } 90 | 91 | module.exports = KeepAlive; 92 | -------------------------------------------------------------------------------- /src/middlewares/last_will.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | let _ = require('underscore'); 4 | 5 | /** 6 | * LastWill Middleware 7 | * 8 | * Manages will packet. 9 | * 10 | * Exposed callbacks: 11 | * - uncleanDisconnect 12 | */ 13 | class LastWill extends Middleware { 14 | /** 15 | * Injects will packet if available on 'uncleanDisconnect'. 16 | * 17 | * @param ctx 18 | * @param __ - not used 19 | * @param callback 20 | */ 21 | uncleanDisconnect(ctx, __, callback) { 22 | let self = this; 23 | 24 | if (ctx.client._last_will) { 25 | let packet = _.defaults(ctx.client._last_will, { 26 | cmd: 'publish' 27 | }); 28 | 29 | setImmediate(function () { 30 | self.stack.process(ctx.client, packet, function () { 31 | }); 32 | }); 33 | 34 | callback(); 35 | } 36 | } 37 | 38 | /** 39 | * Looks for a will packet and stores it. 40 | * 41 | * @param client 42 | * @param packet 43 | * @param next 44 | */ 45 | handle(client, packet, next) { 46 | if (packet.cmd == 'connect') { 47 | if (packet.will) { 48 | client._last_will = packet.will; 49 | } 50 | } 51 | 52 | return next(); 53 | } 54 | } 55 | 56 | module.exports = LastWill; 57 | -------------------------------------------------------------------------------- /src/middlewares/memory_backend.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'), 3 | _ = require('underscore'), 4 | Qlobber = require('qlobber').Qlobber, 5 | qlobber_mqtt_settings = { 6 | separator: '/', 7 | wildcard_one: '+', 8 | wildcard_some: '#' 9 | }; 10 | 11 | /** 12 | * Simple backend with in-memory storage 13 | * 14 | * Exposed callbacks: 15 | * - storeSubscription 16 | * - clearSubscriptions 17 | * - lookupSubscriptions 18 | * - storeRetainedMessage 19 | * - lookupRetainedMessages 20 | * - relayMessage 21 | * - subscribeTopic 22 | * - unsubscribeTopic 23 | * Required Callbacks: 24 | * - forwardMessage 25 | */ 26 | class MemoryBackend extends Middleware { 27 | constructor(config) { 28 | let defaults = { 29 | concurrency: 100 30 | }; 31 | super(config, defaults); 32 | this.sessions = new Map(); 33 | this.offlineMessages = new Map(); 34 | this.retainedMessages = new Qlobber(qlobber_mqtt_settings); 35 | this.pubsub = new Qlobber(qlobber_mqtt_settings); 36 | this.qos_store = new Qlobber(qlobber_mqtt_settings); 37 | this.clientMap = new Map(); 38 | } 39 | 40 | /* SessionManager */ 41 | 42 | _ensureSession(ctx) { 43 | if (!this.sessions.has(ctx.clientId)) { 44 | this.sessions.set(ctx.clientId, new Map()); 45 | } 46 | } 47 | 48 | /** 49 | * Keeps subscription list for client 50 | * 51 | * @param ctx 52 | * @param __ - not used 53 | * @param callback 54 | */ 55 | storeSubscription(ctx, __, callback) { 56 | this._ensureSession(ctx); 57 | this.sessions.get(ctx.clientId).set(ctx.topic, ctx.qos); 58 | callback(); 59 | } 60 | 61 | /** 62 | * Remove subscription from client session 63 | * 64 | * @param ctx 65 | * @param __ 66 | * @param callback 67 | */ 68 | removeSubscription(ctx, __, callback) { 69 | this.sessions.get(ctx.clientId).delete(ctx.topic); 70 | callback(); 71 | } 72 | 73 | /** 74 | * Clears subscription list of client 75 | * 76 | * @param ctx 77 | * @param __ - not used 78 | * @param callback 79 | */ 80 | clearSubscriptions(ctx, __, callback) { 81 | this.sessions.delete(ctx.clientId); 82 | callback(); 83 | } 84 | 85 | /** 86 | * Provides client's current subscription list 87 | * 88 | * @param ctx 89 | * @param store - contains the array of topics subscribed 90 | * @param callback 91 | */ 92 | lookupSubscriptions(ctx, store, callback) { 93 | this._ensureSession(ctx); 94 | this.sessions.get(ctx.clientId).forEach(function (qos, topic) { 95 | store.push({topic, qos}); 96 | }); 97 | callback(); 98 | } 99 | 100 | /** 101 | * Keeps message to be retained for the topic 102 | * 103 | * @param ctx 104 | * @param __ - not used 105 | * @param callback 106 | */ 107 | storeRetainedMessage(ctx, __, callback) { 108 | this.retainedMessages.remove(ctx.topic); 109 | if (ctx.packet.payload !== '') { 110 | this.retainedMessages.add(ctx.topic, ctx.packet); 111 | } 112 | callback(); 113 | } 114 | 115 | /** 116 | * Provides topic's current retained message 117 | * 118 | * @param ctx 119 | * @param store - contains message that retained 120 | * @param callback 121 | */ 122 | lookupRetainedMessages(ctx, store, callback) { 123 | store = this.retainedMessages.match(ctx.topic); 124 | callback(); 125 | } 126 | 127 | /** 128 | * Relay published message to subscribed clients 129 | * 130 | * @param ctx 131 | * @param __ - not used 132 | * @param callback 133 | */ 134 | relayMessage(ctx, __, callback) { 135 | let listeners = _.uniq(this.pubsub.match(ctx.packet.topic)); 136 | _.each(listeners, (listener) => { 137 | let client = this.clientMap.get(listener); 138 | let qos = Math.max(this.qos_store.match(listener + '/' + ctx.packet.topic)); 139 | let packet; 140 | if(_.isUndefined(qos) || qos === ctx.packet.qos) { 141 | packet = ctx.packet; 142 | } 143 | else { 144 | packet = _.clone(ctx.packet); //clone packet since its qos will be modified 145 | packet.qos = qos; 146 | } 147 | if (client) { 148 | this.stack.execute('forwardMessage', { 149 | client: client, 150 | packet: packet 151 | }, callback); 152 | } 153 | else { 154 | this.stack.execute('storeOfflineMessage', { 155 | client: client, 156 | packet: packet 157 | }, callback) 158 | } 159 | }); 160 | if (listeners.length === 0) { 161 | callback(); 162 | } 163 | } 164 | 165 | /** 166 | * Subscribe client to the topic 167 | * 168 | * @param ctx 169 | * @param __ - not used 170 | * @param callback 171 | */ 172 | subscribeTopic(ctx, __, callback) { 173 | this.pubsub.add(ctx.topic, ctx.client._id); 174 | this.qos_store.add(ctx.client._id + '/' + ctx.topic, ctx.qos); 175 | if (!this.clientMap.has(ctx.client._id)) { 176 | this.clientMap.set(ctx.client._id, ctx.client); 177 | } 178 | callback(); 179 | } 180 | 181 | /** 182 | * Unsubscribe client from the topic 183 | * 184 | * @param ctx 185 | * @param __ - not used 186 | * @param callback 187 | */ 188 | unsubscribeTopic(ctx, __, callback) { 189 | this.pubsub.remove(ctx.topic, ctx.client._id); 190 | this.qos_store.remove(ctx.client._id + '/' + ctx.topic); 191 | callback(); 192 | } 193 | 194 | 195 | /** 196 | * Ensures offline message store exists for client 197 | * @param ctx 198 | * @private 199 | */ 200 | _ensureMessageStore(id) { 201 | if (!this.offlineMessages.has(id)) { 202 | this.offlineMessages.set(id, new Map()); 203 | } 204 | } 205 | 206 | /** 207 | * Stores message for offline client 208 | * @param ctx 209 | * @param __ 210 | * @param callback 211 | */ 212 | storeOfflineMessage(ctx, __, callback) { 213 | this._ensureMessageStore(ctx.client._id); 214 | //dont care storing message if it does not have message id 215 | if(ctx.packet.messageId) this.offlineMessages.get(ctx.client._id).set(ctx.packet.messageId, ctx.packet); 216 | callback(); 217 | } 218 | 219 | /** 220 | * Provides client's stored offline messages 221 | * @param ctx 222 | * @param store 223 | * @param callback 224 | */ 225 | lookupOfflineMessages(ctx, store, callback) { 226 | this._ensureMessageStore(ctx.clientId); 227 | this.offlineMessages.get(ctx.clientId).forEach(function (value, key) { 228 | store.push({key, value}); 229 | }); 230 | callback(); 231 | } 232 | 233 | /** 234 | * Removes messages from store. 235 | * @param ctx 236 | * @param __ 237 | * @param callback 238 | */ 239 | removeOfflineMessages(ctx, __, callback) { 240 | this._ensureMessageStore(ctx.clientId); 241 | const messages = this.offlineMessages.get(ctx.clientId); 242 | ctx.messageIds.forEach(function (messageId) { 243 | messages.delete(messageId); 244 | }); 245 | callback(); 246 | } 247 | 248 | /** 249 | * Removes all offline messages of given client. 250 | * @param ctx 251 | * @param __ 252 | * @param callback 253 | */ 254 | clearOfflineMessages(ctx, __, callback) { 255 | this.offlineMessages.delete(ctx.clientId); 256 | callback(); 257 | } 258 | } 259 | 260 | module.exports = MemoryBackend; 261 | -------------------------------------------------------------------------------- /src/middlewares/outbound_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | 4 | /** 5 | * OutboundManager Middleware 6 | * 7 | * Manages outgoing messages. 8 | * 9 | * Exposed callbacks: 10 | * - forwardMessage 11 | */ 12 | class OutboundManager extends Middleware { 13 | /** 14 | * Forward messages to the client. 15 | * 16 | * @param ctx 17 | * @param __ - not used 18 | * @param callback 19 | */ 20 | forwardMessage(ctx, __, callback) { 21 | ctx.client.write({ 22 | cmd: 'publish', 23 | topic: ctx.packet.topic, 24 | payload: ctx.packet.payload, 25 | qos: ctx.packet.qos, 26 | retain: ctx.packet.retain, 27 | messageId: Math.floor(Math.random() * 60000) 28 | }, callback); 29 | } 30 | 31 | handle(client, packet, next, done) { 32 | if (packet.cmd == 'puback') { 33 | //TODO: do something 34 | return done(); 35 | } else { 36 | return next(); 37 | } 38 | } 39 | } 40 | 41 | module.exports = OutboundManager; 42 | -------------------------------------------------------------------------------- /src/middlewares/packet_emitter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | 4 | /** 5 | * PacketEmitter Middleware 6 | * 7 | * Enables legacy handling of clients with events. 8 | */ 9 | class PacketEmitter extends Middleware { 10 | /** 11 | * Sets client handler 12 | * 13 | * @param {function} clientHandler 14 | */ 15 | setClientHandler(clientHandler) { 16 | this.clientHandler = clientHandler; 17 | } 18 | 19 | /** 20 | * Passes the client to the 'clientHandler' 21 | * 22 | * @param client 23 | */ 24 | install(client) { 25 | this.clientHandler(client); 26 | } 27 | 28 | /** 29 | * Emits packet as event. 30 | * 31 | * @param client 32 | * @param packet 33 | * @param next 34 | * @param done 35 | */ 36 | handle(client, packet, next, done) { 37 | client.emit(packet.cmd, packet); 38 | done(); 39 | } 40 | } 41 | 42 | module.exports = PacketEmitter; 43 | -------------------------------------------------------------------------------- /src/middlewares/retain_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | let _ = require('underscore'); 4 | let async = require('async'); 5 | 6 | /** 7 | * RetainManager Middleware 8 | * 9 | * - executes "storeRetainedMessage" for retained messages and resets flag 10 | * - lookups retained messages on "subscribeTopic" and executes "forwardMessage" 11 | */ 12 | class RetainManager extends Middleware { 13 | /** 14 | * Checks for every subscription if there are any retained messages. Executes 15 | * 'lookupRetainedMessages' and 'forwardMessage' with each result. 16 | * 17 | * @param ctx 18 | * @param __ 19 | * @param callback 20 | */ 21 | subscribeTopic(ctx, __, callback) { 22 | let self = this; 23 | 24 | let store = []; 25 | this.stack.execute('lookupRetainedMessages', { 26 | client: ctx.client, 27 | topic: ctx.topic 28 | }, store, function (err) { 29 | if (err) callback(err); 30 | 31 | if (store.length > 0) { 32 | return async.mapSeries(store, function (p, cb) { 33 | return self.stack.execute('forwardMessage', { 34 | client: ctx.client, 35 | packet: p 36 | }, cb); 37 | }, callback); 38 | } else { 39 | return callback(); 40 | } 41 | }); 42 | } 43 | 44 | /** 45 | * Checks for retained publish packets, stores them and clears retained flag. 46 | * Executes 'storeRetainedMessage'. 47 | * 48 | * @param client 49 | * @param packet 50 | * @param next 51 | */ 52 | handle(client, packet, next) { 53 | if (packet.cmd == 'publish') { 54 | if (packet.retain) { 55 | let p = _.clone(packet); 56 | packet.retain = false; 57 | 58 | return this.stack.execute('storeRetainedMessage', { 59 | client: client, 60 | packet: p, 61 | topic: p.topic 62 | }, next); 63 | } else { 64 | return next(); 65 | } 66 | } else { 67 | return next(); 68 | } 69 | } 70 | } 71 | 72 | module.exports = RetainManager; 73 | -------------------------------------------------------------------------------- /src/middlewares/session_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | let async = require('async'); 4 | 5 | /** 6 | * SessionManager Middleware 7 | * 8 | * Manages the clients session and calls callbacks to manage 9 | * the stored subscriptions for clean and unclean clients. 10 | * 11 | * Required Callbacks: 12 | * - storeSubscription 13 | * - lookupSubscriptions 14 | * - clearSubscriptions 15 | * 16 | * Exposed Callbacks: 17 | * - subscribeTopic 18 | * - unsubscribeTopic 19 | */ 20 | class SessionManager extends Middleware { 21 | /** 22 | * Stores subscriptions if the client is unclean. 23 | * 24 | * @param ctx 25 | * @param store 26 | * @param callback 27 | */ 28 | subscribeTopic(ctx, store, callback) { 29 | if (ctx.client._managed_session) { 30 | this.stack.execute('storeSubscription', ctx, callback); 31 | } else { 32 | callback(); 33 | } 34 | } 35 | 36 | /** 37 | * Remove subscription from storage if the client is unclean. 38 | * 39 | * @param ctx 40 | * @param store 41 | * @param callback 42 | */ 43 | unsubscribeTopic(ctx, store, callback) { 44 | if (ctx.client._managed_session) { 45 | this.stack.execute('removeSubscription', ctx, callback); 46 | } else { 47 | callback(); 48 | } 49 | } 50 | 51 | /** 52 | * Checks the clean flag on connect and calls appropriate functions and 53 | * sets clientId as client._id if unclean. 54 | * 55 | * @param client 56 | * @param packet 57 | * @param next 58 | * @param done 59 | */ 60 | handle(client, packet, next, done) { 61 | if (packet.cmd == 'connect') { 62 | client._id = packet.clientId; 63 | if (packet.clean) { 64 | client._managed_session = false; 65 | this._handleCleanClient(client, packet, next, done); 66 | } else { 67 | client._managed_session = true; 68 | this._handleUncleanClient(client, packet, next, done); 69 | } 70 | } else { 71 | return next(); 72 | } 73 | } 74 | 75 | /** 76 | * For clean clients executes 'clearSubscriptions' and sends 'connack' with 77 | * sessionPresent' set to false as there are no subscriptions.. 78 | * 79 | * @param client 80 | * @param packet 81 | * @param next 82 | * @param done 83 | * @private 84 | */ 85 | _handleCleanClient(client, packet, next, done) { 86 | this.stack.execute('clearOfflineMessages', {clientId: packet.clientId}, {}); 87 | this.stack.execute('clearSubscriptions', { 88 | client: client, 89 | packet: packet, 90 | clientId: packet.clientId 91 | }, {}, function (err) { 92 | if (err) return next(err); 93 | 94 | client.write({ 95 | cmd: 'connack', 96 | returnCode: 0, 97 | sessionPresent: false 98 | },done); 99 | }); 100 | } 101 | 102 | /** 103 | * For unclean clients 104 | * executes 'lookupOfflineMessages' and executes 'forwardMessage' 105 | * for each to send them to client 106 | * executes 'lookupSubscriptions' and executes 107 | * 'subscribeTopic' for each and finally sends a 'connack' with 108 | * 'sessionPresent' set to true if there are any subscriptions. 109 | * @param client 110 | * @param packet 111 | * @param next 112 | * @param done 113 | * @private 114 | */ 115 | _handleUncleanClient(client, packet, next, done) { 116 | let self = this; 117 | 118 | let store = []; 119 | 120 | self.stack.execute('lookupOfflineMessages', { 121 | client: client, 122 | clientId: packet.clientId 123 | }, store, function (err) { 124 | if (err) return next(err); 125 | 126 | let sentMessages = []; 127 | 128 | async.mapSeries(store, function (s, cb) { 129 | return self.stack.execute('forwardMessage', { 130 | client, 131 | packet: s.value 132 | }, {}, err => { 133 | if(!err) sentMessages.push(s.key); 134 | cb(err); 135 | }); 136 | }, function (err) { 137 | if (err) return next(err); 138 | 139 | self.stack.execute('removeOfflineMessages', { 140 | clientId: packet.clientId, 141 | messageIds: sentMessages 142 | }, {}, function() { 143 | let subscriptionsStore = []; 144 | self.stack.execute('lookupSubscriptions', { 145 | client: client, 146 | packet: packet, 147 | clientId: packet.clientId 148 | }, subscriptionsStore, function (err) { 149 | if (err) return next(err); 150 | 151 | return async.mapSeries(subscriptionsStore, function (s, cb) { 152 | return self.stack.execute('subscribeTopic', { 153 | client: client, 154 | packet: packet, 155 | topic: s.topic, 156 | qos: s.qos 157 | }, {}, cb); 158 | }, function (err) { 159 | if (err) return next(err); 160 | 161 | return client.write({ 162 | cmd: 'connack', 163 | returnCode: 0, 164 | sessionPresent: (subscriptionsStore.length > 0) 165 | }, done); 166 | }); 167 | }); 168 | 169 | }); 170 | }); 171 | }); 172 | } 173 | } 174 | 175 | module.exports = SessionManager; 176 | -------------------------------------------------------------------------------- /src/middlewares/subscription_manager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Middleware = require('../utils/middleware'); 3 | let async = require('async'); 4 | 5 | /** 6 | * SubscriptionManager Middleware 7 | * 8 | * Handles sunscription and unsubscriptions. 9 | * 10 | * Required callbacks: 11 | * - subscribeTopic 12 | * - unsubscribeTopic 13 | * 14 | * TODO: Forward messages according to the subscriptions and messages max QoS. 15 | * TODO: On Unsubscribe ensure that all QoS1 and QoS2 get completed 16 | */ 17 | class SubscriptionManager extends Middleware { 18 | /** 19 | * Handles 'subscribe' and 'unsubscribe' packets. 20 | * 21 | * @param client 22 | * @param packet 23 | * @param next 24 | * @param done 25 | */ 26 | handle(client, packet, next, done) { 27 | if (packet.cmd == 'subscribe') { 28 | return this._handleSubscription(client, packet, next, done); 29 | } else if (packet.cmd == 'unsubscribe') { 30 | return this._handleUnsubscription(client, packet, next, done); 31 | } else { 32 | return next(); 33 | } 34 | } 35 | 36 | /** 37 | * Executes 'subscribeTopic' for each individual subscription and sends a 'suback'. 38 | * The callback can change the granted subscription by editing 'store.grant'. 39 | * 40 | * @param client 41 | * @param packet 42 | * @param next 43 | * @private 44 | */ 45 | _handleSubscription(client, packet, next, done) { 46 | let self = this; 47 | async.mapSeries(packet.subscriptions, function (s, cb) { 48 | let store = {grant: s.qos}; 49 | return self.stack.execute('subscribeTopic', { 50 | client: client, 51 | packet: packet, 52 | topic: s.topic, 53 | qos: s.qos 54 | }, store, function (err) { 55 | if (err) return cb(err); 56 | 57 | return cb(null, store.grant === false ? 128 : store.grant); 58 | }); 59 | }, function (err, results) { 60 | if (err) return next(err); 61 | 62 | return client.write({ 63 | cmd: 'suback', 64 | messageId: packet.messageId, 65 | granted: results 66 | }, done); 67 | }); 68 | } 69 | 70 | /** 71 | * Executes 'unsubscribeTopic' for each individual unsubscription and sends the 'unsuback'. 72 | * 73 | * @param client 74 | * @param packet 75 | * @param next 76 | * @param done 77 | * @private 78 | */ 79 | _handleUnsubscription(client, packet, next, done) { 80 | let self = this; 81 | return async.mapSeries(packet.unsubscriptions, function (us, cb) { 82 | return self.stack.execute('unsubscribeTopic', { 83 | client: client, 84 | packet: packet, 85 | topic: us 86 | }, cb); 87 | }, function (err) { 88 | if (err) return next(err); 89 | 90 | return client.write({ 91 | cmd: 'unsuback', 92 | messageId: packet.messageId 93 | }, done); 94 | }); 95 | } 96 | } 97 | 98 | module.exports = SubscriptionManager; 99 | -------------------------------------------------------------------------------- /src/stack.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Client = require('./client'); 3 | 4 | function noop() {} 5 | 6 | /** 7 | * Stack Class 8 | * 9 | * Manages a set of middlewares. 10 | */ 11 | class Stack { 12 | /** 13 | * constructor 14 | * 15 | * @param {Function} errorHandler - function that handles all errors 16 | */ 17 | constructor(errorHandler) { 18 | this.middlewares = []; 19 | this.errorHandler = errorHandler || function (err) { 20 | throw err; 21 | }; 22 | } 23 | 24 | /** 25 | * Add a middleware to the stack. 26 | * 27 | * @param middleware - and already instantiated middleware or object 28 | */ 29 | use(middleware) { 30 | middleware.stack = this; 31 | this.middlewares.push(middleware); 32 | } 33 | 34 | /** 35 | * Generates handler that takes streams as an input. 36 | * 37 | * @returns {Function} 38 | */ 39 | handler() { 40 | let self = this; 41 | 42 | return function (stream) { 43 | new Client(self, stream); 44 | } 45 | } 46 | 47 | /** 48 | * Install a client on all middlewares. 49 | * 50 | * @param client - the client that should be installed 51 | */ 52 | install(client) { 53 | this.middlewares.forEach(function (middleware) { 54 | if (middleware.install) { 55 | middleware.install(client); 56 | } 57 | }); 58 | } 59 | 60 | /** 61 | * Run the stack against a client and a single packet. This will be 62 | * automatically called with data from the underlying stream. 63 | * 64 | * The errorHandler gets called with any error ocurring in between. 65 | * 66 | * @param client - the stream emitted the packet 67 | * @param packet - the packet that should be handled 68 | * @param done - to be called on finish 69 | */ 70 | process(client, packet, done) { 71 | let self = this; 72 | 73 | let l = this.middlewares.length; 74 | let i = -1; 75 | 76 | function next(err) { 77 | if (err) { 78 | return self.errorHandler(err, client, packet); 79 | } else { 80 | i++; 81 | if (i < l) { 82 | if(self.middlewares[i].handle) { 83 | return self.middlewares[i].handle(client, packet, next, (done || noop)); 84 | } 85 | else { 86 | return next(); 87 | } 88 | } 89 | else { 90 | return (done || noop)(); 91 | } 92 | } 93 | } 94 | 95 | next(); 96 | } 97 | 98 | /** 99 | * Execute a function on all middlewares. 100 | * 101 | * @param fn - the name of the function 102 | * @param [data] - object passed as first argument 103 | * @param [store] - object passed as second argument (useful to collect data) 104 | * @param [callback] - function to be called after finish unless there is an error 105 | */ 106 | execute(fn, data, store, callback) { 107 | let self = this; 108 | 109 | if (typeof store === 'function') { 110 | callback = store; 111 | store = null; 112 | } 113 | 114 | let l = this.middlewares.length; 115 | let i = -1; 116 | 117 | function next(err) { 118 | if (err) { 119 | return (callback || noop)(err); 120 | } else { 121 | i++; 122 | if (i < l) { 123 | if (self.middlewares[i][fn]) { 124 | return self.middlewares[i][fn](data, store, next, callback); 125 | } else { 126 | return next(); 127 | } 128 | } else { 129 | return (callback || noop)(); 130 | } 131 | } 132 | } 133 | 134 | next(); 135 | } 136 | } 137 | 138 | module.exports = Stack; 139 | -------------------------------------------------------------------------------- /src/utils/middleware.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let _ = require('underscore'); 3 | /** 4 | * Base class that all middlewares should extend 5 | */ 6 | 7 | class Middleware { 8 | constructor(config, defaults) { 9 | this.name = this.constructor.name; 10 | this.config = _.defaults(config || {}, defaults || {}); 11 | } 12 | 13 | handle(client, packet, next) { 14 | next(); 15 | } 16 | } 17 | 18 | module.exports = Middleware; 19 | -------------------------------------------------------------------------------- /src/utils/timer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Timer Class 4 | * 5 | * - manages a setTimout timer 6 | */ 7 | class Timer { 8 | /** 9 | * constructor 10 | * 11 | * @param {Number} timeout 12 | * @param {Function} callback 13 | */ 14 | constructor(timeout, callback) { 15 | this.timeout = timeout; 16 | this.callback = callback; 17 | this.start(); 18 | } 19 | 20 | start() { 21 | let self = this; 22 | if (this.timeout > 0) { 23 | this.timer = setTimeout(function () { 24 | self.callback(); 25 | }, this.timeout); 26 | } 27 | } 28 | 29 | clear() { 30 | if (this.timeout > 0) { 31 | clearTimeout(this.timer); 32 | } 33 | } 34 | 35 | reset() { 36 | this.clear(); 37 | this.start(); 38 | } 39 | } 40 | 41 | module.exports = Timer; 42 | -------------------------------------------------------------------------------- /test/spec_test.js: -------------------------------------------------------------------------------- 1 | global.port = 1883; 2 | 3 | if (process.env['PORT']) { 4 | global.port = process.env['PORT']; 5 | } 6 | 7 | global.hostname = '0.0.0.0'; 8 | 9 | if (process.env['HOSTNAME']) { 10 | global.hostname = process.env['HOSTNAME']; 11 | } 12 | 13 | global.speed = 0.01; 14 | 15 | if (process.env['NORMAL_SPEED']) { 16 | global.speed = 1; 17 | } 18 | 19 | var broker; 20 | var spec = require('../spec/index'); 21 | var TestBroker = require('./test_broker'); 22 | 23 | before(function (done) { 24 | broker = new TestBroker(global.port, global.hostname); 25 | broker.listen(done) 26 | }); 27 | spec.setup({ 28 | host: global.hostname, 29 | port: global.port 30 | }); 31 | spec.registerMochaTests(); 32 | after(function () { 33 | broker.close(); 34 | }); -------------------------------------------------------------------------------- /test/test_broker.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | 3 | var s = require('../index'); 4 | 5 | var Broker = function (port, host) { 6 | var that = this; 7 | this.stack = new s.Stack(function (err) { 8 | console.error(err); 9 | }); 10 | 11 | this.stack.use(new s.MemoryBackend()); 12 | 13 | this.stack.use(new s.Connection({ 14 | forceMQTT4: true 15 | })); 16 | 17 | this.stack.use(new s.KeepAlive({ 18 | grace: global.speed 19 | })); 20 | 21 | this.stack.use(new s.LastWill()); 22 | this.stack.use(new s.SessionManager()); 23 | this.stack.use(new s.RetainManager()); 24 | this.stack.use(new s.InboundManager()); 25 | this.stack.use(new s.OutboundManager()); 26 | this.stack.use(new s.SubscriptionManager()); 27 | 28 | this.server = net.createServer(function (client) { 29 | var handler = that.stack.handler(); 30 | handler(client); 31 | }); 32 | this.server._port = port; 33 | this.server._host = host; 34 | }; 35 | 36 | Broker.prototype.listen = function (done) { 37 | this.server.listen(this.server._port, this.server._host, done); 38 | }; 39 | 40 | Broker.prototype.close = function () { 41 | this.server.close(); 42 | }; 43 | 44 | module.exports = Broker; -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('underscore'); 3 | 4 | describe('Index', function () { 5 | it('should expose classes', function () { 6 | _.each(require('../../index'), function (module) { 7 | assert(typeof module == 'function'); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/unit/middlewares/authentication.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var Authentication = require('../../../src/middlewares/authentication'); 5 | 6 | describe('Authentication', function () { 7 | it('should keep authenticating if unsuccesful', function (done) { 8 | var client = {}; 9 | 10 | client.write = function (packet, cb) { 11 | assert.equal(packet.returnCode, 4); 12 | cb(); 13 | }; 14 | 15 | var packet = { 16 | cmd: 'connect' 17 | }; 18 | 19 | var middleware = new Authentication(); 20 | 21 | stackHelper.mockExecute(middleware, { 22 | authenticateConnection: function (ctx, store, callback) { 23 | assert.equal(ctx.client, client); 24 | assert.equal(ctx.packet, packet); 25 | store.valid = false; 26 | callback(); 27 | } 28 | }); 29 | 30 | middleware.handle(client, packet, function () { 31 | }, function () { 32 | }); 33 | middleware.handle(client, packet, function () { 34 | }, done); 35 | }); 36 | 37 | it('should pass packet if successful', function (done) { 38 | var client = {}; 39 | 40 | var packet = { 41 | cmd: 'connect', 42 | username: 'user', 43 | password: 'pass' 44 | }; 45 | 46 | var middleware = new Authentication(); 47 | 48 | stackHelper.mockExecute(middleware, { 49 | authenticateConnection: function (ctx, store, callback) { 50 | assert.equal(ctx.username, 'user'); 51 | assert.equal(ctx.password, 'pass'); 52 | store.valid = true; 53 | callback(); 54 | } 55 | }); 56 | 57 | middleware.handle(client, packet, done); 58 | }); 59 | 60 | it('should call error handler on error', function (done) { 61 | var client = {}; 62 | 63 | var packet = { 64 | cmd: 'connect' 65 | }; 66 | 67 | var middleware = new Authentication(); 68 | 69 | stackHelper.mockExecute(middleware, { 70 | authenticateConnection: function (ctx, store, callback) { 71 | callback(true); 72 | } 73 | }); 74 | 75 | middleware.handle(client, packet, function (err) { 76 | assert(err); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/unit/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var Authorization = require('../../../src/middlewares/authorization'); 5 | 6 | describe('Authorization', function () { 7 | it('should pass packet on successful authorization', function (done) { 8 | var client = {}; 9 | 10 | var packet = { 11 | cmd: 'test' 12 | }; 13 | 14 | var middleware = new Authorization(); 15 | 16 | stackHelper.mockExecute(middleware, { 17 | authorizePacket: function (ctx, store, callback) { 18 | assert.equal(ctx.client, client); 19 | assert.equal(ctx.packet, packet); 20 | store.valid = true; 21 | callback(); 22 | } 23 | }); 24 | 25 | middleware.handle(client, packet, done); 26 | }); 27 | 28 | it('should dismiss packet on unsuccessful authorization', function (done) { 29 | var client = {}; 30 | 31 | var packet = { 32 | cmd: 'test' 33 | }; 34 | 35 | var middleware = new Authorization(); 36 | 37 | stackHelper.mockExecute(middleware, { 38 | authorizePacket: function (ctx, store, callback) { 39 | assert.equal(ctx.client, client); 40 | assert.equal(ctx.packet, packet); 41 | callback(); 42 | } 43 | }); 44 | 45 | middleware.handle(client, packet, function () { 46 | }, done); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/unit/middlewares/connection.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var stream = require('stream'); 3 | 4 | var stackHelper = require('../stack_helper'); 5 | var Connection = require('../../../src/middlewares/connection'); 6 | 7 | describe('Connection', function () { 8 | it('should close client if first packet is not a "connect"', function (done) { 9 | var client = new stream.Duplex(); 10 | 11 | var called = false; 12 | 13 | client.close = function () { 14 | called = true; 15 | }; 16 | 17 | var middleware = new Connection(); 18 | 19 | middleware.install(client); 20 | 21 | middleware.handle(client, { 22 | cmd: 'test' 23 | }, function () { 24 | }, function () { 25 | assert(called); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should close client if "connect" is sent more than once', function (done) { 31 | var client = new stream.Duplex(); 32 | 33 | var called = false; 34 | 35 | client.close = function () { 36 | called = true; 37 | }; 38 | 39 | var middleware = new Connection(); 40 | 41 | middleware.install(client); 42 | 43 | middleware.stack = { 44 | execute: function (fn, ctx, callback) { 45 | assert.equal(fn, 'uncleanDisconnect'); 46 | assert.equal(ctx.client, client); 47 | callback(); 48 | } 49 | }; 50 | 51 | middleware.handle(client, { 52 | cmd: 'connect' 53 | }, function () { 54 | }, function () { 55 | }); 56 | 57 | middleware.handle(client, { 58 | cmd: 'connect' 59 | }, function () { 60 | }, function () { 61 | assert(called); 62 | done(); 63 | }); 64 | }); 65 | 66 | it("should close client and emit 'cleanDisconnect' on 'disconnect' package", function (done) { 67 | var client = new stream.Duplex(); 68 | 69 | var called = false; 70 | 71 | client.close = function () { 72 | called = true; 73 | }; 74 | 75 | var middleware = new Connection(); 76 | 77 | middleware.install(client); 78 | 79 | middleware.stack = { 80 | execute: function (fn, ctx, callback) { 81 | assert.equal(fn, 'cleanDisconnect'); 82 | assert.equal(ctx.client, client); 83 | callback(); 84 | } 85 | }; 86 | 87 | middleware.handle(client, { 88 | cmd: 'connect' 89 | }, function () { 90 | }, function () { 91 | }); 92 | 93 | middleware.handle(client, { 94 | cmd: 'disconnect' 95 | }, function () { 96 | }, function () { 97 | assert(called); 98 | done(); 99 | }); 100 | }); 101 | 102 | it("should emit 'uncleanDisconnect' on 'close' event", function (done) { 103 | var client = new stream.Duplex(); 104 | 105 | client.close = function () { 106 | }; 107 | 108 | var middleware = new Connection(); 109 | 110 | middleware.install(client); 111 | 112 | middleware.stack = { 113 | execute: function (fn, ctx) { 114 | assert.equal(fn, 'uncleanDisconnect'); 115 | assert.equal(ctx.client, client); 116 | done(); 117 | } 118 | }; 119 | 120 | client.emit('close'); 121 | }); 122 | 123 | it("should close client and emit 'uncleanDisconnect' on 'error' event", function (done) { 124 | var client = new stream.Duplex(); 125 | 126 | var called = false; 127 | 128 | client.close = function () { 129 | called = true; 130 | }; 131 | 132 | var middleware = new Connection(); 133 | 134 | middleware.install(client); 135 | 136 | middleware.stack = { 137 | execute: function (fn, ctx) { 138 | assert.equal(fn, 'uncleanDisconnect'); 139 | assert(called); 140 | done(); 141 | } 142 | }; 143 | 144 | client.emit('error'); 145 | }); 146 | 147 | it('should close client if protocol is not mqtt 4', function (done) { 148 | var client = new stream.Duplex(); 149 | 150 | var called = false; 151 | 152 | client.close = function () { 153 | called = true; 154 | }; 155 | 156 | var middleware = new Connection({ 157 | forceMQTT4: true 158 | }); 159 | 160 | middleware.install(client); 161 | 162 | middleware.handle(client, { 163 | cmd: 'connet', 164 | protocolId: 'hello' 165 | }, function () { 166 | }, function () { 167 | assert(called); 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should close client if "closeClient" has been called', function (done) { 173 | var middleware = new Connection(); 174 | 175 | stackHelper.mockExecute(middleware, { 176 | uncleanDisconnect: function (ctx) { 177 | } 178 | }); 179 | 180 | middleware.closeClient({ 181 | client: { 182 | close: function () { 183 | done(); 184 | } 185 | } 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/unit/middlewares/inbound_manager.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var InboundManager = require('../../../src/middlewares/inbound_manager'); 5 | 6 | describe('InboundManager', function () { 7 | it('should call "relayMessage"', function (done) { 8 | var stream = {}; 9 | 10 | var packet = { 11 | cmd: 'publish', 12 | topic: 'hello', 13 | payload: 'cool' 14 | }; 15 | 16 | var middleware = new InboundManager(); 17 | 18 | stackHelper.mockExecute(middleware, { 19 | relayMessage: function (ctx) { 20 | assert.equal(stream, ctx.client); 21 | assert.equal(packet, ctx.packet); 22 | assert.equal(packet.topic, ctx.topic); 23 | assert.equal(packet.payload, ctx.payload); 24 | done(); 25 | } 26 | }); 27 | 28 | middleware.handle(stream, packet); 29 | }); 30 | 31 | it('should send "puback" on QoS 1', function (done) { 32 | var stream = {}; 33 | 34 | stream.write = function (_, cb) { 35 | cb(); 36 | }; 37 | 38 | var packet = { 39 | cmd: 'publish', 40 | qos: 1 41 | }; 42 | 43 | var middleware = new InboundManager(); 44 | 45 | stackHelper.mockExecute(middleware, { 46 | relayMessage: function (ctx, __, callback) { 47 | callback(); 48 | } 49 | }); 50 | 51 | middleware.handle(stream, packet, function () { 52 | }, done); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/middlewares/keep_alive.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var KeepAlive = require('../../../src/middlewares/keep_alive'); 4 | 5 | describe('KeepAlive', function () { 6 | it('should respond to pingreq', function (done) { 7 | var client = {}; 8 | 9 | client.write = function (_, cb) { 10 | cb(); 11 | }; 12 | 13 | var middleware = new KeepAlive(); 14 | 15 | middleware.handle(client, { 16 | cmd: 'pingreq' 17 | }, function () { 18 | }, done); 19 | }); 20 | 21 | it("should start default timer and close connection on inactivity", function (done) { 22 | var client = {}; 23 | 24 | client.destroy = function () { 25 | client.on('uncleanDisconnect', done); 26 | }; 27 | 28 | var middleware = new KeepAlive({ 29 | defaultTimeout: 0.001 30 | }); 31 | 32 | middleware.stack = { 33 | execute: function (fn, ctx) { 34 | assert.equal(fn, 'closeClient'); 35 | assert.equal(ctx.client, client); 36 | done(); 37 | } 38 | }; 39 | 40 | middleware.install(client); 41 | }); 42 | 43 | it("should restart timer and close connection on inactivity", function (done) { 44 | var client = {}; 45 | 46 | client.destroy = function () { 47 | client.on('uncleanDisconnect', done); 48 | }; 49 | 50 | var middleware = new KeepAlive(); 51 | 52 | middleware.stack = { 53 | execute: function (fn, ctx) { 54 | assert.equal(fn, 'closeClient'); 55 | assert.equal(ctx.client, client); 56 | done(); 57 | } 58 | }; 59 | 60 | middleware.handle(client, { 61 | cmd: 'connect', 62 | keepalive: 0.001 63 | }, function () { 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/unit/middlewares/last_will.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var LastWill = require('../../../src/middlewares/last_will'); 4 | 5 | describe('LastWill', function () { 6 | it("should cache lastWill and inject on 'uncleanDisconnect'", function (done) { 7 | var client = {}; 8 | 9 | var packet = { 10 | cmd: 'connect', 11 | will: { 12 | value: 1 13 | } 14 | }; 15 | 16 | var middleware = new LastWill(); 17 | 18 | middleware.stack = { 19 | process: function (_client, _packet) { 20 | assert(_client, client); 21 | assert.equal(_packet, packet.will); 22 | done(); 23 | } 24 | }; 25 | 26 | middleware.handle(client, packet, function () { 27 | }); 28 | middleware.uncleanDisconnect({ 29 | client: client 30 | }, [], function () { 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/middlewares/outbound_manager.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var OutboundManager = require('../../../src/middlewares/outbound_manager'); 5 | 6 | describe('OutboundManager', function () { 7 | it('should forward packets when "forwardMessage" is executed', function (done) { 8 | var stream = {}; 9 | 10 | var packet = { 11 | cmd: 'publish', 12 | topic: 'cool', 13 | payload: 'cool', 14 | qos: 0 15 | }; 16 | 17 | var middleware = new OutboundManager(); 18 | 19 | stream.write = function (_packet) { 20 | assert.equal(_packet.topic, packet.topic); 21 | assert.equal(_packet.payload, packet.payload); 22 | assert.equal(_packet.qos, packet.qos); 23 | done(); 24 | }; 25 | 26 | middleware.forwardMessage({ 27 | client: stream, 28 | packet: packet 29 | }); 30 | }); 31 | 32 | it('should handle "puback" on QoS 1', function (done) { 33 | var stream = {}; 34 | 35 | var packet = { 36 | cmd: 'publish', 37 | topic: 'cool', 38 | payload: 'cool', 39 | qos: 1 40 | }; 41 | 42 | var packet2 = { 43 | cmd: 'puback', 44 | messageId: 10 45 | }; 46 | 47 | var middleware = new OutboundManager(); 48 | 49 | stream.write = function () { 50 | middleware.handle(stream, packet2, function () { 51 | }, done); 52 | }; 53 | 54 | middleware.forwardMessage({ 55 | client: stream, 56 | packet: packet 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/unit/middlewares/packet_emitter.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | var PacketEmitter = require('../../../src/middlewares/packet_emitter'); 5 | 6 | describe('PacketEmitter', function () { 7 | it('should emit packets as events', function (done) { 8 | var myclient = new EventEmitter(); 9 | 10 | var packet = { 11 | cmd: 'test', 12 | value: 1 13 | }; 14 | 15 | var middleware = new PacketEmitter(); 16 | middleware.setClientHandler(function (client) { 17 | client.on('test', function (_packet) { 18 | assert.equal(_packet, packet); 19 | }); 20 | }); 21 | 22 | middleware.install(myclient); 23 | middleware.handle(myclient, packet, function () { 24 | }, done); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/unit/middlewares/retain_manager.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var RetainManager = require('../../../src/middlewares/retain_manager'); 5 | 6 | describe('RetainManager', function () { 7 | it('should store retained messages and reset flag', function (done) { 8 | var stream = {}; 9 | 10 | var packet = { 11 | cmd: 'publish', 12 | topic: 'cool', 13 | payload: 'cool', 14 | qos: 0, 15 | retain: true 16 | }; 17 | 18 | var middleware = new RetainManager(); 19 | 20 | stackHelper.mockExecute(middleware, { 21 | storeRetainedMessage: function (ctx, __, callback) { 22 | assert.equal(ctx.packet.retain, true); 23 | callback(); 24 | } 25 | }); 26 | 27 | middleware.handle(stream, packet, function () { 28 | assert.equal(packet.retain, false); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('should lookup retained messages on subscribe', function (done) { 34 | var stream = {}; 35 | 36 | var packet = { 37 | topic: 'foo', 38 | payload: 'bar', 39 | qos: 1, 40 | retain: true 41 | }; 42 | 43 | var middleware = new RetainManager(); 44 | 45 | stackHelper.mockExecute(middleware, { 46 | lookupRetainedMessages: function (ctx, store, callback) { 47 | store.push(packet); 48 | callback(); 49 | }, 50 | forwardMessage: function (ctx) { 51 | assert.deepEqual(ctx.packet, packet); 52 | done(); 53 | } 54 | }); 55 | 56 | middleware.subscribeTopic({ 57 | client: stream, 58 | topic: 'foo' 59 | }, function () { 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/unit/middlewares/session_manager.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var SessionManager = require('../../../src/middlewares/session_manager'); 5 | 6 | describe('SessionManager', function () { 7 | it('should call clearSubscriptions for clean client', function (done) { 8 | var stream = {}; 9 | 10 | stream.write = function (data, callback) { 11 | assert(!packet.sessionPresent); 12 | callback && callback(); 13 | }; 14 | 15 | var packet = { 16 | cmd: 'connect', 17 | clean: true, 18 | clientId: 'foo' 19 | }; 20 | 21 | var middleware = new SessionManager(); 22 | 23 | stackHelper.mockExecute(middleware, { 24 | clearOfflineMessages: function (ctx, __, callback) { 25 | assert(ctx.clientId, 'foo'); 26 | callback && callback(); 27 | }, 28 | clearSubscriptions: function (ctx, __, callback) { 29 | assert(ctx.client, stream); 30 | assert(ctx.packet, packet); 31 | callback(); 32 | } 33 | }); 34 | 35 | middleware.handle(stream, packet, function () { 36 | }, done); 37 | }); 38 | 39 | it('should call lookupSubscriptions for unclean client', function (done) { 40 | var stream = {}; 41 | 42 | stream.write = function (packet, cb) { 43 | assert(packet.sessionPresent); 44 | cb(); 45 | }; 46 | 47 | var packet = { 48 | cmd: 'connect', 49 | clientId: 'foo', 50 | clean: false 51 | }; 52 | 53 | var packet2 = { 54 | topic: 'bar', 55 | qos: 0 56 | }; 57 | 58 | var middleware = new SessionManager(); 59 | 60 | stackHelper.mockExecute(middleware, { 61 | lookupOfflineMessages: function (ctx, store, callback) { 62 | assert(ctx.clientId, 'foo'); 63 | assert(ctx.client, stream); 64 | callback(); 65 | }, 66 | forwardMessage: function (ctx, store, callback) { 67 | callback(); 68 | }, 69 | removeOfflineMessages: function (ctx, store, callback) { 70 | callback(); 71 | }, 72 | lookupSubscriptions: function (ctx, store, callback) { 73 | assert(ctx.client, stream); 74 | assert(ctx.packet, packet); 75 | store.push(packet2); 76 | callback(); 77 | }, 78 | subscribeTopic: function (ctx, store, callback) { 79 | assert(ctx.client, stream); 80 | assert(ctx.packet, packet2); 81 | assert.deepEqual(store, {}); 82 | callback(); 83 | } 84 | }); 85 | 86 | middleware.handle(stream, packet, function () { 87 | }, done); 88 | }); 89 | 90 | it('should call storeSubscriptions for new subscriptions', function (done) { 91 | var stream = {}; 92 | 93 | stream.write = function () { 94 | }; 95 | 96 | var packet = { 97 | topic: 'bar', 98 | qos: 0 99 | }; 100 | 101 | var middleware = new SessionManager(); 102 | 103 | stackHelper.mockExecute(middleware, { 104 | lookupOfflineMessages: function (ctx, store, callback) { 105 | callback(); 106 | }, 107 | forwardMessage: function (ctx, store, callback) { 108 | callback(); 109 | }, 110 | removeOfflineMessages: function (ctx, store, callback) { 111 | callback(); 112 | }, 113 | lookupSubscriptions: function (ctx, store, callback) { 114 | callback(); 115 | }, 116 | storeSubscription: function (ctx, __, callback) { 117 | assert(ctx.client, stream); 118 | assert(ctx.packet, packet); 119 | callback(); 120 | } 121 | }); 122 | 123 | middleware.handle(stream, { 124 | cmd: 'connect', 125 | clientId: 'foo', 126 | clean: false 127 | }, function () { 128 | }, function () { 129 | }); 130 | 131 | middleware.subscribeTopic({ 132 | client: stream, 133 | packet: packet 134 | }, {}, done); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/unit/middlewares/subscription_manager.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var stackHelper = require('../stack_helper'); 4 | var SubscriptionManager = require('../../../src/middlewares/subscription_manager'); 5 | 6 | describe('SubscriptionManager', function () { 7 | it('should execute "subscribeTopic" for one subscription', function (done) { 8 | var stream = {}; 9 | 10 | var packet = { 11 | cmd: 'subscribe', 12 | subscriptions: [{ 13 | topic: 'foo', 14 | qos: 1 15 | }] 16 | }; 17 | 18 | stream.write = function (packet, cb) { 19 | assert.deepEqual(packet.granted, [1]); 20 | cb(); 21 | }; 22 | 23 | var middleware = new SubscriptionManager(); 24 | 25 | stackHelper.mockExecute(middleware, { 26 | subscribeTopic: function (ctx, store, callback) { 27 | assert.equal(ctx.client, stream); 28 | assert.equal(ctx.topic, 'foo'); 29 | assert.equal(ctx.qos, 1); 30 | callback(); 31 | } 32 | }); 33 | 34 | middleware.handle(stream, packet, function () { 35 | }, done); 36 | }); 37 | 38 | it('should execute "subscribeTopic" for multiple subscription', function (done) { 39 | var stream = {}; 40 | 41 | var packet = { 42 | cmd: 'subscribe', 43 | subscriptions: [{ 44 | topic: 'foo', 45 | qos: 1 46 | }, { 47 | topic: 'bar', 48 | qos: 0 49 | }, { 50 | topic: 'baz', 51 | qos: 2 52 | }] 53 | }; 54 | 55 | stream.write = function (packet, cb) { 56 | assert.deepEqual(packet.granted, [1, 0, 2]); 57 | cb(); 58 | }; 59 | 60 | var middleware = new SubscriptionManager(); 61 | 62 | stackHelper.mockExecute(middleware, { 63 | subscribeTopic: function (ctx, store, callback) { 64 | callback(); 65 | } 66 | }); 67 | 68 | middleware.handle(stream, packet, function () { 69 | }, done); 70 | }); 71 | 72 | it('should execute "unsubscribeTopic" for each unsubscription', function (done) { 73 | var stream = {}; 74 | 75 | var packet = { 76 | cmd: 'unsubscribe', 77 | unsubscriptions: ['foo'] 78 | }; 79 | 80 | stream.write = function (_, cb) { 81 | cb(); 82 | }; 83 | 84 | var middleware = new SubscriptionManager(); 85 | 86 | stackHelper.mockExecute(middleware, { 87 | unsubscribeTopic: function (ctx, __, callback) { 88 | assert.equal(ctx.client, stream); 89 | assert.equal(ctx.topic, 'foo'); 90 | callback(); 91 | } 92 | }); 93 | 94 | middleware.handle(stream, packet, function () { 95 | }, done); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/unit/stack.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var stream = require('stream'); 3 | 4 | var Stack = require('../../src/stack'); 5 | 6 | describe('Stack', function () { 7 | it("should run stack when data is available", function (done) { 8 | var client = {}; 9 | 10 | var stack = new Stack(); 11 | 12 | stack.use({ 13 | handle: function (_client, packet, next) { 14 | assert.equal(_client, client); 15 | assert.equal(packet, 'hello'); 16 | next(); 17 | } 18 | }); 19 | 20 | stack.use({ 21 | handle: function (_client, packet, next, done) { 22 | assert.equal(_client, client); 23 | assert.equal(packet, 'hello'); 24 | done(); 25 | } 26 | }); 27 | 28 | stack.process(client, 'hello', done); 29 | }); 30 | 31 | it("should call error handler on error", function (done) { 32 | var client = {}; 33 | 34 | var stack = new Stack(function (err) { 35 | assert.equal(err, 'error'); 36 | done(); 37 | }); 38 | 39 | stack.use({ 40 | handle: function (_client, packet, next) { 41 | assert.equal(_client, client); 42 | assert.equal(packet, 'hello'); 43 | next('error'); 44 | } 45 | }); 46 | 47 | stack.use({ 48 | handle: function () { 49 | assert(false); 50 | } 51 | }); 52 | 53 | stack.process(client, 'hello', function () { 54 | }); 55 | }); 56 | 57 | it("should call install for each client", function (done) { 58 | var client = {}; 59 | var stack = new Stack(); 60 | 61 | stack.use({ 62 | install: function (_client) { 63 | assert.equal(_client, client); 64 | } 65 | }); 66 | 67 | stack.use({ 68 | install: function (_client) { 69 | assert.equal(_client, client); 70 | done(); 71 | } 72 | }); 73 | 74 | stack.install(client); 75 | }); 76 | 77 | it('should set the stack on the middleware', function () { 78 | var stack = new Stack(); 79 | var middleware = {}; 80 | stack.use(middleware); 81 | assert(middleware.stack, stack); 82 | }); 83 | 84 | it('should execute a function on all middlewares and collect responses', function (done) { 85 | var stack = new Stack(); 86 | 87 | stack.use({ 88 | testFunction: function (n, store, callback) { 89 | store.push(n + 1); 90 | callback(); 91 | } 92 | }); 93 | 94 | stack.use({ 95 | testFunction: function (n, store, callback) { 96 | store.push(n + 2); 97 | callback(); 98 | } 99 | }); 100 | 101 | var store = []; 102 | 103 | stack.execute('testFunction', 1, store, function () { 104 | assert.deepEqual(store, [2, 3]); 105 | done(); 106 | }); 107 | }); 108 | 109 | it('should execute a function on all middlewares even if there are none', function (done) { 110 | var stack = new Stack(); 111 | stack.execute('testFunction', 1, done); 112 | }); 113 | 114 | it('should execute a function on all middlewares and catch errors', function (done) { 115 | var stack = new Stack(); 116 | 117 | stack.use({ 118 | testFunction: function (n, __, callback) { 119 | callback(); 120 | } 121 | }); 122 | 123 | stack.use({ 124 | testFunction: function (n, __, callback) { 125 | callback(new Error('fail')); 126 | } 127 | }); 128 | 129 | stack.execute('testFunction', 1, function (err) { 130 | assert(err); 131 | done(); 132 | }); 133 | }); 134 | 135 | it('should execute a function on all middlewares and provide common store', function (done) { 136 | var stack = new Stack(); 137 | 138 | stack.use({ 139 | testFunction: function (_, store, callback) { 140 | store[0]++; 141 | callback(); 142 | } 143 | }); 144 | 145 | stack.use({ 146 | testFunction: function (_, store, callback) { 147 | store[0]++; 148 | callback(); 149 | } 150 | }); 151 | 152 | var store = [1]; 153 | 154 | stack.execute('testFunction', 1, store, function (err) { 155 | assert(!err); 156 | assert.equal(store, 3); 157 | done(); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/unit/stack_helper.js: -------------------------------------------------------------------------------- 1 | module.exports.mockExecute = function (middleware, config) { 2 | middleware.stack = { 3 | execute: function (fn, ctx, store, cb) { 4 | if (typeof store === 'function' && typeof cb === 'undefined') { 5 | cb = store; 6 | store = null; 7 | } 8 | 9 | config[fn](ctx, store, cb); 10 | } 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /test/unit/utils/timer.js: -------------------------------------------------------------------------------- 1 | var Timer = require('../../../src/utils/timer'); 2 | 3 | describe('Timer', function () { 4 | it('should call callback', function (done) { 5 | var timer = new Timer(1, function () { 6 | timer.clear(); 7 | done(); 8 | }); 9 | timer.start(); 10 | }); 11 | }); 12 | --------------------------------------------------------------------------------