UNPKG

mqtt-reqres-broker

Version:

mqtt request/response websocket broker

283 lines (206 loc) 7.52 kB
// ## MqttReqResBroker module.exports = function () { 'use strict'; // dependencies const debug = require('debug')('mqtt-reqres-broker'); const aedes = require('aedes'); const http = require('http'); const ws = require('websocket-stream'); const noop = function () {}; // limit the maximum payload size const MAX_PAYLOAD_LENGTH = 1024 * 512; // ## broker class class MqttReqResBroker { // ### constructor constructor () { // debug('constructor()'); this.hostname = null; this.port = null; this.mqtt = null; this.httpServer = null; this.wss = null; } // ctor // ### initialize(options, callback) initialize (options, callback) { debug('initialize', options); this.hostname = options.hostname; this.port = options.port; if (this.mqtt) { this.mqtt.close(); } // for options see: https://github.com/mcollina/aedes#aedesopts this.mqtt = aedes(); /* authorizeSubscribe It will be called when a client publishes a message. see: https://github.com/mcollina/aedes#instanceauthorizesubscribeclient-pattern-doneerr-pattern */ this.mqtt.authorizeSubscribe = function (client, sub, cb) { debug('%s authorizeSubscribe(%s, %s)', client.id, client.id, sub.topic); try { if (sub.topic.length > 500) { throw new Error('ESUBTOPIC'); } const topicParts = sub.topic.split('/'); if (sub.qos !== 0) { throw new Error('ESUBQOS'); } if (topicParts[0] === 'connect') { if (topicParts[1] && topicParts[2] === '+' && topicParts.length === 3) { // subscribe -t connect/<device-id>/+ debug('%s authorizeSubscribe() subscribed connect/%s/+', client.id, topicParts[1]); } else { throw(new Error('ESUBCONNECT')); } } else if (topicParts[0] === 'message') { if (topicParts[1] && topicParts[2] && topicParts[3] === '#' && topicParts.length === 4) { // subscribe -t message/<from-client-id>/<to-channel-id>/# debug('%s authorizeSubscribe() subscribed message/%s/%s/#', client.id, topicParts[1], topicParts[2]); } else { throw(new Error('ESUBMESSAGE')); } } else { throw(new Error('ESUBTOPIC')); } } catch (error) { // To negate a subscription, set the subscription to null sub = null; debug('%s authorizeSubscribe() error:', client.id, error); } cb(null, sub); }; /* authorizePublish It will be called when a client publishes a message. see: https://github.com/mcollina/aedes#instanceauthorizesubscribeclient-pattern-doneerr-pattern */ this.mqtt.authorizePublish = function (client, packet, cb) { debug('%s authorizePublish(%s, %s)', client.id, client.id, packet.topic); var error = null; try { if (packet.topic.length > 500) { throw new Error('EPUBTOPIC'); } if (packet.payload.length > MAX_PAYLOAD_LENGTH) { throw new Error('EPUBPAYLOAD'); } const topicParts = packet.topic.split('/'); if (packet.qos !== 0) { throw new Error('EPUBQOS'); } if (packet.retain) { throw new Error('EPUBRETAIN'); } if (topicParts[0] === 'connect') { if (topicParts[1] && topicParts[2] && topicParts.length === 3) { // publish -t connect/<to-device-id>/<from-client-id> debug('%s authorizePublish() published connect/%s/%s', client.id, topicParts[1], topicParts[2]); } else { throw(new Error('EPUBCONNECT')); } } else if (topicParts[0] === 'message') { if (topicParts[1] && topicParts[2] && topicParts.length >= 3) { // publish -t message/<from-client-id>/<to-channel-id> debug('%s authorizePublish() published message/%s/%s', client.id, topicParts[1], topicParts[2]); } else { throw(new Error('EPUBMESSAGE')); } } else { throw(new Error('EPUBTOPIC')); } } catch (e) { error = e; debug('%s authorizePublish() error:', client.id, error); } cb(error); }; // initialize done callback(); return this; } // initialize // ### initializeMQTTEventHandlers (options, callback) initializeMQTTEventHandlers (options, callback) { debug('initializeMQTTEventHandlers()'); const mqtt = this.mqtt; // for event descriptions see https://github.com/mcollina/aedes#aedesopts mqtt.on('subscribe', (topics, client) => { if (client) { debug( '%s on subscribe topics', client.id ); topics.forEach(topic => { debug( '%s topic "%s" qos: %s', client.id, topic.topic, topic.qos ); }); } }); mqtt.on('publish', function (packet, client) { if (client) { debug( '%s on publish message topic "%s" qos %s', client.id, packet.topic, packet.qos // packet.payload.toString() ); } }); mqtt.on('clientError', function (client, err) { debug('%s client error', client.id, err.message, err.stack); }); mqtt.on('client', function (client) { debug('%s on client - a new Client connects', client.id); }); callback(); return this; } // initializeMQTTEventHandlers // ### start (options, callback) start (options, callback) { debug('start()'); this.httpServer = http.createServer(); this.wss = ws.createServer({ server: this.httpServer }, this.mqtt.handle); // init mqtt event handlers this.initializeMQTTEventHandlers(null, () => { // app.listen(port, [hostname], [backlog], [callback]) this.httpServer.listen(this.port, this.hostname, callback); }); return this; } // start // ###close (callback) close (callback) { callback = callback || noop; if (this.mqtt) { this.mqtt.close(); } if (this.wss) { this.wss.close(); } if (this.httpServer) { // this.httpServer.unref(); // Stops the server from accepting new connections and keeps existing connections. This function is asynchronous, the server is finally closed when all connections are ended and the server emits a 'close' event. The optional callback will be called once the 'close' event occurs. Unlike that event, it will be called with an Error as its only argument if the server was not open when it was closed. // see: https://nodejs.org/dist/latest-v6.x/docs/api/net.html#net_server_close_callback this.httpServer.close(callback); } else { callback(); } return this; } // close } // MqttReqResBroker return MqttReqResBroker; }();