UNPKG

rocket.chat.mqtt

Version:

It's a MQTT Server, using redis to scale horizontally.

322 lines (266 loc) 7.73 kB
'use strict' var Buffer = require('safe-buffer').Buffer var mqemitter = require('mqemitter') var EE = require('events').EventEmitter var util = require('util') var memory = require('aedes-persistence') var parallel = require('fastparallel') var series = require('fastseries') var shortid = require('shortid') var Packet = require('aedes-packet') var bulk = require('bulk-write-stream') var reusify = require('reusify') var Client = require('./lib/client') var xtend = require('xtend') module.exports = Aedes var defaultOptions = { concurrency: 100, heartbeatInterval: 60000, // 1 minute connectTimeout: 30000, // 30 secs authenticate: defaultAuthenticate, authorizePublish: defaultAuthorizePublish, authorizeSubscribe: defaultAuthorizeSubscribe, authorizeForward: defaultAuthorizeForward, published: defaultPublished } function Aedes (opts) { var that = this if (!(this instanceof Aedes)) { return new Aedes(opts) } opts = xtend(defaultOptions, opts) this.id = shortid() this.counter = 0 this.connectTimeout = opts.connectTimeout this.mq = opts.mq || mqemitter(opts) this.handle = function handle (conn) { conn.setMaxListeners(opts.concurrency * 2) // return, just to please standard return new Client(that, conn) } this.persistence = opts.persistence || memory() this.persistence.broker = this this._parallel = parallel() this._series = series() this._enqueuers = reusify(DoEnqueues) this.authenticate = opts.authenticate this.authorizePublish = opts.authorizePublish this.authorizeSubscribe = opts.authorizeSubscribe this.authorizeForward = opts.authorizeForward this.published = opts.published this.clients = {} this.brokers = {} var heartbeatTopic = '$SYS/' + that.id + '/heartbeat' this._heartbeatInterval = setInterval(heartbeat, opts.heartbeatInterval) var bufId = Buffer.from(that.id, 'utf8') function heartbeat () { that.publish({ topic: heartbeatTopic, payload: bufId }, noop) } function deleteOldBrokers (broker) { if (that.brokers[broker] + (3 * opts.heartbeatInterval) < Date.now()) { delete that.brokers[broker] } } this._clearWillInterval = setInterval(function () { Object.keys(that.brokers).forEach(deleteOldBrokers) that.persistence .streamWill(that.brokers) .pipe(bulk.obj(receiveWills)) }, opts.heartbeatInterval * 4) function receiveWills (chunks, done) { that._parallel(that, checkAndPublish, chunks, done) } function checkAndPublish (will, done) { var needsPublishing = !that.brokers[will.brokerId] || that.brokers[will.brokerId] + (3 * opts.heartbeatInterval) < Date.now() if (needsPublishing) { // randomize this, so that multiple brokers // do not publish the same wills at the same time that.publish(will, function publishWill (err) { if (err) { return done(err) } that.persistence.delWill({ id: will.clientId, brokerId: will.brokerId }, done) }) } else { done() } } this.mq.on('$SYS/+/heartbeat', function storeBroker (packet, done) { that.brokers[packet.payload.toString()] = Date.now() done() }) this.mq.on('$SYS/+/new/clients', function closeSameClients (packet, done) { var serverId = packet.topic.split('/')[1] var clientId = packet.payload.toString() if (that.clients[clientId] && serverId !== that.id) { that.clients[clientId].close(done) } else { done() } }) // metadata this.connectedClients = 0 } util.inherits(Aedes, EE) function storeRetained (_, done) { var packet = this.packet if (packet.retain) { this.broker.persistence.storeRetained(packet, done) } else { done() } } function emitPacket (_, done) { this.packet.retain = false this.broker.mq.emit(this.packet, done) } function enqueueOffline (_, done) { var packet = this.packet var enqueuer = this.broker._enqueuers.get() enqueuer.complete = done enqueuer.status = this enqueuer.topic = packet.topic this.broker.persistence.subscriptionsByTopic( packet.topic, enqueuer.done ) } function DoEnqueues () { this.next = null this.status = null this.complete = null this.topic = null var that = this this.done = function doneEnqueue (err, subs) { var status = that.status var broker = status.broker if (err) { // is this really recoverable? // let's just error the whole aedes broker.emit('error', err) } else { var complete = that.complete if (that.topic.indexOf('$SYS') === 0) { subs = subs.filter(removeSharp) } that.status = null that.complete = null that.topic = null broker.persistence.outgoingEnqueueCombi(subs, status.packet, complete) broker._enqueuers.release(that) } } } // + is 43 // # is 35 function removeSharp (sub) { var code = sub.topic.charCodeAt(0) return code !== 43 && code !== 35 } function callPublished (_, done) { this.broker.published(this.packet, this.client, done) this.broker.emit('publish', this.packet, this.client) } var publishFuncsSimple = [ storeRetained, emitPacket, callPublished ] var publishFuncsQoS = [ storeRetained, enqueueOffline, emitPacket, callPublished ] Aedes.prototype.publish = function (packet, client, done) { if (typeof client === 'function') { done = client client = null } var p = new Packet(packet, this) var publishFuncs = publishFuncsSimple if (p.qos > 0) { publishFuncs = publishFuncsQoS } this._series(new PublishState(this, client, p), publishFuncs, null, done) } Aedes.prototype.subscribe = function (topic, func, done) { this.mq.on(topic, func, done) } Aedes.prototype.unsubscribe = function (topic, func, done) { this.mq.removeListener(topic, func, done) } Aedes.prototype.registerClient = function (client) { var that = this if (this.clients[client.id]) { // moving out so we wait for this, so we don't // unregister a good client this.clients[client.id].close(function closeClient () { that._finishRegisterClient(client) }) } else { this._finishRegisterClient(client) } } Aedes.prototype._finishRegisterClient = function (client) { this.connectedClients++ this.clients[client.id] = client this.emit('client', client) this.publish({ topic: '$SYS/' + this.id + '/new/clients', payload: Buffer.from(client.id, 'utf8') }, noop) } Aedes.prototype.unregisterClient = function (client) { this.connectedClients-- delete this.clients[client.id] this.emit('clientDisconnect', client) this.publish({ topic: '$SYS/' + this.id + '/disconnect/clients', payload: Buffer.from(client.id, 'utf8') }, noop) } function closeClient (client, cb) { this.clients[client].close(cb) } Aedes.prototype.close = function (cb) { var that = this clearInterval(this._heartbeatInterval) clearInterval(this._clearWillInterval) this._parallel(this, closeClient, Object.keys(this.clients), doneClose) function doneClose () { that.emit('closed') cb = cb || noop cb() } } function defaultAuthenticate (client, username, password, callback) { callback(null, true) } function defaultAuthorizePublish (client, packet, callback) { callback(null) } function defaultAuthorizeSubscribe (client, sub, callback) { callback(null, sub) } function defaultAuthorizeForward (client, packet) { return packet } function defaultPublished (packet, client, callback) { callback(null) } function PublishState (broker, client, packet) { this.broker = broker this.client = client this.packet = packet } function noop () {}