387 lines
9.7 KiB
JavaScript
387 lines
9.7 KiB
JavaScript
'use strict'
|
|
|
|
const EventEmitter = require('events')
|
|
const util = require('util')
|
|
const parallel = require('fastparallel')
|
|
const series = require('fastseries')
|
|
const { v4: uuidv4 } = require('uuid')
|
|
const reusify = require('reusify')
|
|
const { pipeline } = require('stream')
|
|
const Packet = require('aedes-packet')
|
|
const memory = require('aedes-persistence')
|
|
const mqemitter = require('mqemitter')
|
|
const Client = require('./lib/client')
|
|
const { $SYS_PREFIX, bulk } = require('./lib/utils')
|
|
|
|
module.exports = Aedes.createBroker = Aedes
|
|
|
|
const defaultOptions = {
|
|
concurrency: 100,
|
|
heartbeatInterval: 60000, // 1 minute
|
|
connectTimeout: 30000, // 30 secs
|
|
decodeProtocol: null,
|
|
preConnect: defaultPreConnect,
|
|
authenticate: defaultAuthenticate,
|
|
authorizePublish: defaultAuthorizePublish,
|
|
authorizeSubscribe: defaultAuthorizeSubscribe,
|
|
authorizeForward: defaultAuthorizeForward,
|
|
published: defaultPublished,
|
|
trustProxy: false,
|
|
trustedProxies: [],
|
|
queueLimit: 42,
|
|
maxClientsIdLength: 23,
|
|
keepaliveLimit: 0
|
|
}
|
|
|
|
function Aedes (opts) {
|
|
const that = this
|
|
|
|
if (!(this instanceof Aedes)) {
|
|
return new Aedes(opts)
|
|
}
|
|
|
|
opts = Object.assign({}, defaultOptions, opts)
|
|
|
|
this.id = opts.id || uuidv4()
|
|
// +1 when construct a new aedes-packet
|
|
// internal track for last brokerCounter
|
|
this.counter = 0
|
|
this.queueLimit = opts.queueLimit
|
|
this.connectTimeout = opts.connectTimeout
|
|
this.keepaliveLimit = opts.keepaliveLimit
|
|
this.maxClientsIdLength = opts.maxClientsIdLength
|
|
this.mq = opts.mq || mqemitter({
|
|
concurrency: opts.concurrency,
|
|
matchEmptyLevels: true // [MQTT-4.7.1-3]
|
|
})
|
|
this.handle = function handle (conn, req) {
|
|
conn.setMaxListeners(opts.concurrency * 2)
|
|
// create a new Client instance for a new connection
|
|
// return, just to please standard
|
|
return new Client(that, conn, req)
|
|
}
|
|
this.persistence = opts.persistence || memory()
|
|
this.persistence.broker = this
|
|
this._parallel = parallel()
|
|
this._series = series()
|
|
this._enqueuers = reusify(DoEnqueues)
|
|
|
|
this.preConnect = opts.preConnect
|
|
this.authenticate = opts.authenticate
|
|
this.authorizePublish = opts.authorizePublish
|
|
this.authorizeSubscribe = opts.authorizeSubscribe
|
|
this.authorizeForward = opts.authorizeForward
|
|
this.published = opts.published
|
|
|
|
this.decodeProtocol = opts.decodeProtocol
|
|
this.trustProxy = opts.trustProxy
|
|
this.trustedProxies = opts.trustedProxies
|
|
|
|
this.clients = {}
|
|
this.brokers = {}
|
|
|
|
const heartbeatTopic = $SYS_PREFIX + that.id + '/heartbeat'
|
|
const birthTopic = $SYS_PREFIX + that.id + '/birth'
|
|
|
|
this._heartbeatInterval = setInterval(heartbeat, opts.heartbeatInterval)
|
|
|
|
const bufId = Buffer.from(that.id, 'utf8')
|
|
|
|
// in a cluster env this is used to warn other broker instances
|
|
// that this broker is alive
|
|
that.publish({
|
|
topic: birthTopic,
|
|
payload: bufId
|
|
}, noop)
|
|
|
|
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)
|
|
|
|
pipeline(
|
|
that.persistence.streamWill(that.brokers),
|
|
bulk(receiveWills),
|
|
function done (err) {
|
|
if (err) {
|
|
that.emit('error', err)
|
|
}
|
|
}
|
|
)
|
|
}, opts.heartbeatInterval * 4)
|
|
|
|
function receiveWills (chunks, done) {
|
|
that._parallel(that, checkAndPublish, chunks, done)
|
|
}
|
|
|
|
function checkAndPublish (will, done) {
|
|
const notPublish =
|
|
that.brokers[will.brokerId] !== undefined && that.brokers[will.brokerId] + (3 * opts.heartbeatInterval) >= Date.now()
|
|
|
|
if (notPublish) return done()
|
|
|
|
// randomize this, so that multiple brokers
|
|
// do not publish the same wills at the same time
|
|
this.authorizePublish(that.clients[will.clientId] || null, will, function (err) {
|
|
if (err) { return doneWill() }
|
|
that.publish(will, doneWill)
|
|
|
|
function doneWill (err) {
|
|
if (err) { return done(err) }
|
|
that.persistence.delWill({
|
|
id: will.clientId,
|
|
brokerId: will.brokerId
|
|
}, done)
|
|
}
|
|
})
|
|
}
|
|
|
|
this.mq.on($SYS_PREFIX + '+/heartbeat', function storeBroker (packet, done) {
|
|
that.brokers[packet.payload.toString()] = Date.now()
|
|
done()
|
|
})
|
|
|
|
this.mq.on($SYS_PREFIX + '+/birth', function brokerBorn (packet, done) {
|
|
const brokerId = packet.payload.toString()
|
|
|
|
// reset duplicates counter
|
|
if (brokerId !== that.id) {
|
|
for (const clientId in that.clients) {
|
|
delete that.clients[clientId].duplicates[brokerId]
|
|
}
|
|
}
|
|
|
|
done()
|
|
})
|
|
|
|
this.mq.on($SYS_PREFIX + '+/new/clients', function closeSameClients (packet, done) {
|
|
const serverId = packet.topic.split('/')[1]
|
|
const clientId = packet.payload.toString()
|
|
|
|
if (that.clients[clientId] && serverId !== that.id) {
|
|
if (that.clients[clientId].closed) {
|
|
// remove the client from the list if it is already closed
|
|
that.deleteClient(clientId)
|
|
done()
|
|
} else {
|
|
that.clients[clientId].close(done)
|
|
}
|
|
} else {
|
|
done()
|
|
}
|
|
})
|
|
|
|
// metadata
|
|
this.connectedClients = 0
|
|
this.closed = false
|
|
}
|
|
|
|
util.inherits(Aedes, EventEmitter)
|
|
|
|
function storeRetained (packet, done) {
|
|
if (packet.retain) {
|
|
this.broker.persistence.storeRetained(packet, done)
|
|
} else {
|
|
done()
|
|
}
|
|
}
|
|
|
|
function emitPacket (packet, done) {
|
|
if (this.client) packet.clientId = this.client.id
|
|
this.broker.mq.emit(packet, done)
|
|
}
|
|
|
|
function enqueueOffline (packet, done) {
|
|
const enqueuer = this.broker._enqueuers.get()
|
|
|
|
enqueuer.complete = done
|
|
enqueuer.packet = packet
|
|
enqueuer.topic = packet.topic
|
|
enqueuer.broker = this.broker
|
|
this.broker.persistence.subscriptionsByTopic(
|
|
packet.topic,
|
|
enqueuer.done
|
|
)
|
|
}
|
|
|
|
function DoEnqueues () {
|
|
this.next = null
|
|
this.complete = null
|
|
this.packet = null
|
|
this.topic = null
|
|
this.broker = null
|
|
|
|
const that = this
|
|
|
|
this.done = function doneEnqueue (err, subs) {
|
|
const broker = that.broker
|
|
|
|
if (err) {
|
|
// is this really recoverable?
|
|
// let's just error the whole aedes
|
|
// https://nodejs.org/api/events.html#events_error_events
|
|
broker.emit('error', err)
|
|
return
|
|
}
|
|
|
|
if (that.topic.indexOf($SYS_PREFIX) === 0) {
|
|
subs = subs.filter(removeSharp)
|
|
}
|
|
|
|
const packet = that.packet
|
|
const complete = that.complete
|
|
|
|
that.packet = null
|
|
that.complete = null
|
|
that.topic = null
|
|
|
|
broker.persistence.outgoingEnqueueCombi(subs, packet, complete)
|
|
broker._enqueuers.release(that)
|
|
}
|
|
}
|
|
|
|
// + is 43
|
|
// # is 35
|
|
function removeSharp (sub) {
|
|
const 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)
|
|
}
|
|
|
|
const publishFuncsSimple = [
|
|
storeRetained,
|
|
emitPacket,
|
|
callPublished
|
|
]
|
|
const publishFuncsQoS = [
|
|
storeRetained,
|
|
enqueueOffline,
|
|
emitPacket,
|
|
callPublished
|
|
]
|
|
Aedes.prototype.publish = function (packet, client, done) {
|
|
if (typeof client === 'function') {
|
|
done = client
|
|
client = null
|
|
}
|
|
const p = new Packet(packet, this)
|
|
const publishFuncs = p.qos > 0 ? publishFuncsQoS : publishFuncsSimple
|
|
|
|
this._series(new PublishState(this, client, packet), publishFuncs, p, 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) {
|
|
const that = this
|
|
if (this.clients[client.id]) {
|
|
// [MQTT-3.1.4-2]
|
|
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_PREFIX + this.id + '/new/clients',
|
|
payload: Buffer.from(client.id, 'utf8')
|
|
}, noop)
|
|
}
|
|
|
|
Aedes.prototype.unregisterClient = function (client) {
|
|
this.deleteClient(client.id)
|
|
this.emit('clientDisconnect', client)
|
|
this.publish({
|
|
topic: $SYS_PREFIX + this.id + '/disconnect/clients',
|
|
payload: Buffer.from(client.id, 'utf8')
|
|
}, noop)
|
|
}
|
|
|
|
Aedes.prototype.deleteClient = function (clientId) {
|
|
this.connectedClients--
|
|
delete this.clients[clientId]
|
|
}
|
|
|
|
function closeClient (client, cb) {
|
|
this.clients[client].close(cb)
|
|
}
|
|
|
|
Aedes.prototype.close = function (cb = noop) {
|
|
const that = this
|
|
if (this.closed) {
|
|
return cb()
|
|
}
|
|
this.closed = true
|
|
clearInterval(this._heartbeatInterval)
|
|
clearInterval(this._clearWillInterval)
|
|
this._parallel(this, closeClient, Object.keys(this.clients), doneClose)
|
|
function doneClose () {
|
|
that.emit('closed')
|
|
that.mq.close(cb)
|
|
}
|
|
}
|
|
|
|
Aedes.prototype.version = require('./package.json').version
|
|
|
|
function defaultPreConnect (client, packet, callback) {
|
|
callback(null, true)
|
|
}
|
|
|
|
function defaultAuthenticate (client, username, password, callback) {
|
|
callback(null, true)
|
|
}
|
|
|
|
function defaultAuthorizePublish (client, packet, callback) {
|
|
if (packet.topic.startsWith($SYS_PREFIX)) {
|
|
return callback(new Error($SYS_PREFIX + ' topic is reserved'))
|
|
}
|
|
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 () {}
|