首次提交
This commit is contained in:
+399
@@ -0,0 +1,399 @@
|
||||
'use strict'
|
||||
|
||||
const mqtt = require('mqtt-packet')
|
||||
const EventEmitter = require('events')
|
||||
const util = require('util')
|
||||
const eos = require('end-of-stream')
|
||||
const Packet = require('aedes-packet')
|
||||
const write = require('./write')
|
||||
const QoSPacket = require('./qos-packet')
|
||||
const handleSubscribe = require('./handlers/subscribe')
|
||||
const handleUnsubscribe = require('./handlers/unsubscribe')
|
||||
const handle = require('./handlers')
|
||||
const { pipeline } = require('stream')
|
||||
const { through } = require('./utils')
|
||||
|
||||
module.exports = Client
|
||||
|
||||
function Client (broker, conn, req) {
|
||||
const that = this
|
||||
|
||||
// metadata
|
||||
this.closed = false
|
||||
this.connecting = false
|
||||
this.connected = false
|
||||
this.connackSent = false
|
||||
this.errored = false
|
||||
|
||||
// mqtt params
|
||||
this.id = null
|
||||
this.clean = true
|
||||
this.version = null
|
||||
|
||||
this.subscriptions = {}
|
||||
this.duplicates = {}
|
||||
|
||||
this.broker = broker
|
||||
this.conn = conn
|
||||
conn.client = this
|
||||
|
||||
this._disconnected = false
|
||||
this._authorized = false
|
||||
this._parsingBatch = 1
|
||||
this._nextId = Math.ceil(Math.random() * 65535)
|
||||
|
||||
this.req = req
|
||||
this.connDetails = req ? req.connDetails : null
|
||||
|
||||
// we use two variables for the will
|
||||
// because we store in _will while
|
||||
// we are authenticating
|
||||
this.will = null
|
||||
this._will = null
|
||||
|
||||
this._parser = mqtt.parser()
|
||||
this._parser.client = this
|
||||
this._parser._queue = [] // queue packets received before client fires 'connect' event. Prevents memory leaks on 'connect' event
|
||||
this._parser.on('packet', enqueue)
|
||||
this.once('connected', dequeue)
|
||||
|
||||
function nextBatch (err) {
|
||||
if (err) {
|
||||
that.emit('error', err)
|
||||
return
|
||||
}
|
||||
|
||||
const client = that
|
||||
|
||||
if (client._paused) {
|
||||
return
|
||||
}
|
||||
|
||||
that._parsingBatch--
|
||||
if (that._parsingBatch <= 0) {
|
||||
that._parsingBatch = 0
|
||||
const buf = client.conn.read(null)
|
||||
if (buf) {
|
||||
client._parser.parse(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
this._nextBatch = nextBatch
|
||||
|
||||
conn.on('readable', nextBatch)
|
||||
|
||||
this.on('error', onError)
|
||||
conn.on('error', this.emit.bind(this, 'error'))
|
||||
this._parser.on('error', this.emit.bind(this, 'error'))
|
||||
|
||||
conn.on('end', this.close.bind(this))
|
||||
this._eos = eos(this.conn, this.close.bind(this))
|
||||
|
||||
const getToForwardPacket = (_packet) => {
|
||||
// Mqttv5 3.8.3.1: https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html#_Toc3901169
|
||||
// prevent to forward messages sent by the same client when no-local flag is set
|
||||
if (_packet.clientId === that.id && _packet.nl) return
|
||||
|
||||
const toForward = dedupe(that, _packet) &&
|
||||
that.broker.authorizeForward(that, _packet)
|
||||
|
||||
return toForward
|
||||
}
|
||||
|
||||
this.deliver0 = function deliverQoS0 (_packet, cb) {
|
||||
const toForward = getToForwardPacket(_packet)
|
||||
|
||||
if (toForward) {
|
||||
// Give nodejs some time to clear stacks, or we will see
|
||||
// "Maximum call stack size exceeded" in a very high load
|
||||
setImmediate(() => {
|
||||
const packet = new Packet(toForward, broker)
|
||||
packet.qos = 0
|
||||
write(that, packet, function (err) {
|
||||
that._onError(err)
|
||||
cb() // don't pass the error here or it will be thrown by mqemitter
|
||||
})
|
||||
})
|
||||
} else {
|
||||
setImmediate(cb)
|
||||
}
|
||||
}
|
||||
|
||||
this.deliverQoS = function deliverQoS (_packet, cb) {
|
||||
// downgrade to qos0 if requested by publish
|
||||
if (_packet.qos === 0) {
|
||||
that.deliver0(_packet, cb)
|
||||
return
|
||||
}
|
||||
const toForward = getToForwardPacket(_packet)
|
||||
|
||||
if (toForward) {
|
||||
setImmediate(() => {
|
||||
const packet = new QoSPacket(toForward, that)
|
||||
// Downgrading to client subscription qos if needed
|
||||
const clientSub = that.subscriptions[packet.topic]
|
||||
if (clientSub && (clientSub.qos || 0) < packet.qos) {
|
||||
packet.qos = clientSub.qos
|
||||
}
|
||||
packet.writeCallback = cb
|
||||
if (that.clean || packet.retain) {
|
||||
writeQoS(null, that, packet)
|
||||
} else {
|
||||
broker.persistence.outgoingUpdate(that, packet, writeQoS)
|
||||
}
|
||||
})
|
||||
} else if (that.clean === false) {
|
||||
that.broker.persistence.outgoingClearMessageId(that, _packet, noop)
|
||||
// we consider this to be an error, since the packet is undefined
|
||||
// so there's nothing to send
|
||||
setImmediate(cb)
|
||||
} else {
|
||||
setImmediate(cb)
|
||||
}
|
||||
}
|
||||
|
||||
this._keepaliveTimer = null
|
||||
this._keepaliveInterval = -1
|
||||
|
||||
this._connectTimer = setTimeout(function () {
|
||||
that.emit('error', new Error('connect did not arrive in time'))
|
||||
}, broker.connectTimeout)
|
||||
}
|
||||
|
||||
function dedupe (client, packet) {
|
||||
const id = packet.brokerId
|
||||
if (!id) {
|
||||
return true
|
||||
}
|
||||
const duplicates = client.duplicates
|
||||
const counter = packet.brokerCounter
|
||||
const result = (duplicates[id] || 0) < counter
|
||||
if (result) {
|
||||
duplicates[id] = counter
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function writeQoS (err, client, packet) {
|
||||
if (err) {
|
||||
// is this right, or we should ignore thins?
|
||||
client.emit('error', err)
|
||||
// don't pass the error here or it will be thrown by mqemitter
|
||||
packet.writeCallback()
|
||||
} else {
|
||||
write(client, packet, function (err) {
|
||||
if (err) {
|
||||
client.emit('error', err)
|
||||
}
|
||||
// don't pass the error here or it will be thrown by mqemitter
|
||||
packet.writeCallback()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function drainRequest (req) {
|
||||
req.callback()
|
||||
}
|
||||
|
||||
function onError (err) {
|
||||
if (!err) return
|
||||
|
||||
this.errored = true
|
||||
this.conn.removeAllListeners('error')
|
||||
this.conn.on('error', noop)
|
||||
// hack to clean up the write callbacks in case of error
|
||||
const state = this.conn._writableState
|
||||
const list = typeof state.getBuffer === 'function' ? state.getBuffer() : state.buffer
|
||||
list.forEach(drainRequest)
|
||||
this.broker.emit(this.id ? 'clientError' : 'connectionError', this, err)
|
||||
this.close()
|
||||
}
|
||||
|
||||
util.inherits(Client, EventEmitter)
|
||||
|
||||
Client.prototype._onError = onError
|
||||
|
||||
Client.prototype.publish = function (message, done) {
|
||||
const packet = new Packet(message, this.broker)
|
||||
const that = this
|
||||
if (packet.qos === 0) {
|
||||
// skip offline and send it as it is
|
||||
this.deliver0(packet, done)
|
||||
return
|
||||
}
|
||||
if (!this.clean && this.id) {
|
||||
this.broker.persistence.outgoingEnqueue({
|
||||
clientId: this.id
|
||||
}, packet, function deliver (err) {
|
||||
if (err) {
|
||||
return done(err)
|
||||
}
|
||||
that.deliverQoS(packet, done)
|
||||
})
|
||||
} else {
|
||||
that.deliverQoS(packet, done)
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.subscribe = function (packet, done) {
|
||||
if (!packet.subscriptions) {
|
||||
if (!Array.isArray(packet)) {
|
||||
packet = [packet]
|
||||
}
|
||||
packet = {
|
||||
subscriptions: packet
|
||||
}
|
||||
}
|
||||
handleSubscribe(this, packet, false, done)
|
||||
}
|
||||
|
||||
Client.prototype.unsubscribe = function (packet, done) {
|
||||
if (!packet.unsubscriptions) {
|
||||
if (!Array.isArray(packet)) {
|
||||
packet = [packet]
|
||||
}
|
||||
packet = {
|
||||
unsubscriptions: packet
|
||||
}
|
||||
}
|
||||
handleUnsubscribe(this, packet, done)
|
||||
}
|
||||
|
||||
Client.prototype.close = function (done) {
|
||||
if (this.closed) {
|
||||
if (typeof done === 'function') {
|
||||
done()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const that = this
|
||||
const conn = this.conn
|
||||
|
||||
this.closed = true
|
||||
|
||||
this._parser.removeAllListeners('packet')
|
||||
conn.removeAllListeners('readable')
|
||||
|
||||
this._parser._queue = null
|
||||
|
||||
if (this._keepaliveTimer) {
|
||||
this._keepaliveTimer.clear()
|
||||
this._keepaliveInterval = -1
|
||||
this._keepaliveTimer = null
|
||||
}
|
||||
|
||||
if (this._connectTimer) {
|
||||
clearTimeout(this._connectTimer)
|
||||
this._connectTimer = null
|
||||
}
|
||||
|
||||
this._eos()
|
||||
this._eos = noop
|
||||
|
||||
handleUnsubscribe(
|
||||
this,
|
||||
{
|
||||
unsubscriptions: Object.keys(this.subscriptions)
|
||||
},
|
||||
finish)
|
||||
|
||||
function finish () {
|
||||
const will = that.will
|
||||
// _disconnected is set only if client is disconnected with a valid disconnect packet
|
||||
if (!that._disconnected && will) {
|
||||
that.broker.authorizePublish(that, will, function (err) {
|
||||
if (err) { return done() }
|
||||
that.broker.publish(will, that, done)
|
||||
|
||||
function done () {
|
||||
that.broker.persistence.delWill({
|
||||
id: that.id,
|
||||
brokerId: that.broker.id
|
||||
}, noop)
|
||||
}
|
||||
})
|
||||
} else if (will) {
|
||||
// delete the persisted will even on clean disconnect https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349232
|
||||
that.broker.persistence.delWill({
|
||||
id: that.id,
|
||||
brokerId: that.broker.id
|
||||
}, noop)
|
||||
}
|
||||
|
||||
that.will = null // this function might be called twice
|
||||
that._will = null
|
||||
|
||||
that.connected = false
|
||||
that.connecting = false
|
||||
|
||||
conn.removeAllListeners('error')
|
||||
conn.on('error', noop)
|
||||
|
||||
if (that.broker.clients[that.id] && that._authorized) {
|
||||
that.broker.unregisterClient(that)
|
||||
}
|
||||
|
||||
// clear up the drain event listeners
|
||||
that.conn.emit('drain')
|
||||
that.conn.removeAllListeners('drain')
|
||||
|
||||
conn.destroy()
|
||||
|
||||
if (typeof done === 'function') {
|
||||
done()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.pause = function () {
|
||||
this._paused = true
|
||||
}
|
||||
|
||||
Client.prototype.resume = function () {
|
||||
this._paused = false
|
||||
this._nextBatch()
|
||||
}
|
||||
|
||||
function enqueue (packet) {
|
||||
const client = this.client
|
||||
client._parsingBatch++
|
||||
// already connected or it's the first packet
|
||||
if (client.connackSent || client._parsingBatch === 1) {
|
||||
handle(client, packet, client._nextBatch)
|
||||
} else {
|
||||
if (this._queue.length < client.broker.queueLimit) {
|
||||
this._queue.push(packet)
|
||||
} else {
|
||||
this.emit('error', new Error('Client queue limit reached'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dequeue () {
|
||||
const q = this._parser._queue
|
||||
if (q) {
|
||||
for (let i = 0, len = q.length; i < len; i++) {
|
||||
handle(this, q[i], this._nextBatch)
|
||||
}
|
||||
|
||||
this._parser._queue = null
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.emptyOutgoingQueue = function (done) {
|
||||
const client = this
|
||||
const persistence = client.broker.persistence
|
||||
|
||||
function filter (packet, enc, next) {
|
||||
persistence.outgoingClearMessageId(client, packet, next)
|
||||
}
|
||||
|
||||
pipeline(
|
||||
persistence.outgoingStream(client),
|
||||
through(filter),
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
function noop () {}
|
||||
Reference in New Issue
Block a user