首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+762
View File
@@ -0,0 +1,762 @@
'use strict'
const { test } = require('tap')
const http = require('http')
const ws = require('websocket-stream')
const mqtt = require('mqtt')
const { setup, connect, delay } = require('./helper')
const aedes = require('../')
;[{ ver: 3, id: 'MQIsdp' }, { ver: 4, id: 'MQTT' }].forEach(function (ele) {
test('connect and connack (minimal)', function (t) {
t.plan(2)
const s = setup()
t.teardown(s.broker.close.bind(s.broker))
s.inStream.write({
cmd: 'connect',
protocolId: ele.id,
protocolVersion: ele.ver,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.same(packet, {
cmd: 'connack',
returnCode: 0,
length: 2,
qos: 0,
retain: false,
dup: false,
topic: null,
payload: null,
sessionPresent: false
}, 'successful connack')
t.equal(s.client.version, ele.ver)
})
})
})
// [MQTT-3.1.2-2]
test('reject client requested for unacceptable protocol version', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQIsdp',
protocolVersion: 5,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 1, 'unacceptable protocol version')
t.equal(broker.connectedClients, 0)
})
broker.on('clientError', function (client, err) {
t.fail('should not raise clientError error')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'unacceptable protocol version')
})
})
// [MQTT-3.1.2-1], Guarded in mqtt-packet
test('reject client requested for unsupported protocol version', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 2,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(client.version, null)
t.equal(err.message, 'Invalid protocol version')
t.equal(broker.connectedClients, 0)
})
})
test('reject clients that exceed the keepalive limit', function (t) {
t.plan(3)
const broker = aedes({
keepaliveLimit: 100
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
keepalive: 150
})
s.outStream.on('data', function (packet) {
console.log(packet)
t.same(packet, {
cmd: 'connack',
returnCode: 6,
length: 2,
qos: 0,
retain: false,
dup: false,
topic: null,
payload: null,
sessionPresent: false
}, 'unsuccessful connack, keep alive limit exceeded')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'keep alive limit exceeded')
t.equal(broker.connectedClients, 0)
})
})
// Guarded in mqtt-packet
test('reject clients with no clientId running on MQTT 3.1.0', function (t) {
t.plan(3)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQIsdp',
protocolVersion: 3,
clean: true,
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(client.version, null)
t.equal(err.message, 'clientId must be supplied before 3.1.1')
t.equal(broker.connectedClients, 0)
})
})
// [MQTT-3.1.3-7], Guarded in mqtt-packet
test('reject clients without clientid and clean=false on MQTT 3.1.1', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: false,
clientId: '',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'clientId must be given if cleanSession set to 0')
t.equal(broker.connectedClients, 0)
})
})
test('clients without clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 4)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
broker.on('client', function (client) {
t.ok(client.id.startsWith('aedes_'))
})
})
test('client connect error while fetching subscriptions', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
broker.persistence.subscriptionsByClient = function (c, cb) {
cb(new Error('error'), [], c)
}
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: false,
clientId: 'my-client',
keepalive: 0
})
broker.on('clientError', function (client, err) {
t.equal(client.version, 4)
t.pass('throws error')
})
})
test('client connect clear outgoing', function (t) {
t.plan(1)
const clientId = 'abcde'
const brokerId = 'pippo'
const broker = aedes({ id: brokerId })
t.teardown(broker.close.bind(broker))
const subs = [{ clientId }]
const packet = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 1,
brokerId,
brokerCounter: 2,
retain: true,
messageId: 42,
dup: false
}
broker.persistence.outgoingEnqueueCombi(subs, packet, function () {
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId,
keepalive: 0
})
broker.on('clientReady', function (client) {
broker.persistence.outgoingUpdate(client, packet, function (err) {
t.equal('no such packet', err.message, 'packet not found')
})
})
})
})
test('clients with zero-byte clientid and clean=true on MQTT 3.1.1 will get a generated clientId', function (t) {
t.plan(5)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: '',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 4)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
broker.on('client', function (client) {
t.ok(client.id.startsWith('aedes_'))
})
})
// [MQTT-3.1.3-7]
test('reject clients with > 23 clientId length in MQTT 3.1.0', function (t) {
t.plan(7)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
const conn = s.client.conn
const end = conn.end
conn.end = function () {
t.fail('should not call `conn.end()`')
end()
}
function drain () {
t.pass('should empty connection request queue')
}
conn._writableState.getBuffer = () => [{ callback: drain }, { callback: drain }]
s.inStream.write({
cmd: 'connect',
protocolId: 'MQIsdp',
protocolVersion: 3,
clean: true,
clientId: 'abcdefghijklmnopqrstuvwxyz',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 2, 'identifier rejected')
t.equal(broker.connectedClients, 0)
t.equal(s.client.version, null)
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'identifier rejected')
})
})
test('connect clients with > 23 clientId length using aedes maxClientsIdLength option in MQTT 3.1.0', function (t) {
t.plan(4)
const broker = aedes({ maxClientsIdLength: 26 })
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 3,
clean: true,
clientId: 'abcdefghijklmnopqrstuvwxyz',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 3)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
})
test('connect with > 23 clientId length in MQTT 3.1.1', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'abcdefghijklmnopqrstuvwxyz',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.equal(packet.cmd, 'connack')
t.equal(packet.returnCode, 0)
t.equal(broker.connectedClients, 1)
t.equal(s.client.version, 4)
})
broker.on('connectionError', function (client, err) {
t.error(err, 'no error')
})
})
// [MQTT-3.1.0-1]
test('the first Packet MUST be a CONNECT Packet', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const packet = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false
}
const s = setup(broker)
s.inStream.write(packet)
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'Invalid protocol')
})
setImmediate(() => {
t.ok(s.conn.destroyed, 'close connection if first packet is not a CONNECT')
s.conn.destroy()
})
})
// [MQTT-3.1.0-2]
test('second CONNECT Packet sent from a Client as a protocol violation and disconnect the Client', function (t) {
t.plan(4)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const packet = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client',
keepalive: 0
}
broker.on('clientError', function (client, err) {
t.equal(err.message, 'Invalid protocol')
})
const s = connect(setup(broker), { clientId: 'abcde' })
s.broker.on('clientReady', function () {
t.ok(broker.clients.abcde.connected)
// destory client when there is a 2nd cmd:connect, even the clientId is dfferent
s.inStream.write(packet)
setImmediate(() => {
t.equal(broker.clients.abcde, undefined, 'client instance is removed')
t.ok(s.conn.destroyed, 'close connection if packet is a CONNECT after network is established')
})
})
})
test('connect handler calls done when preConnect throws error', function (t) {
t.plan(1)
const broker = aedes({
preConnect: function (client, packet, done) {
done(Error('error in preconnect'))
}
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
const handleConnect = require('../lib/handlers/connect')
handleConnect(s.client, {}, function done (err) {
t.equal(err.message, 'error in preconnect', 'calls done with error')
})
})
test('handler calls done when disconnect or unknown packet cmd is received', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
const handle = require('../lib/handlers/index')
handle(s.client, { cmd: 'disconnect' }, function done () {
t.pass('calls done when disconnect cmd is received')
})
handle(s.client, { cmd: 'fsfadgragae' }, function done () {
t.pass('calls done when unknown cmd is received')
})
})
test('reject second CONNECT Packet sent while first CONNECT still in preConnect stage', function (t) {
t.plan(2)
const packet1 = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client-1',
keepalive: 0
}
const packet2 = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client-2',
keepalive: 0
}
let i = 0
const broker = aedes({
preConnect: function (client, packet, done) {
const ms = i++ === 0 ? 2000 : 500
setTimeout(function () {
done(null, true)
}, ms)
}
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
broker.on('connectionError', function (client, err) {
t.equal(err.info.clientId, 'my-client-2')
t.equal(err.message, 'Invalid protocol')
})
const msg = async (s, ms, msg) => {
await delay(ms)
s.inStream.write(msg)
}
;(async () => {
await Promise.all([msg(s, 100, packet1), msg(s, 200, packet2)])
})().catch(
(error) => {
t.fail(error)
}
)
})
// [MQTT-3.1.2-1], Guarded in mqtt-packet
test('reject clients with wrong protocol name', function (t) {
t.plan(2)
const broker = aedes()
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT_hello',
protocolVersion: 3,
clean: true,
clientId: 'my-client',
keepalive: 0
})
s.outStream.on('data', function (packet) {
t.fail('no data sent')
})
broker.on('connectionError', function (client, err) {
t.equal(err.message, 'Invalid protocolId')
t.equal(broker.connectedClients, 0)
})
})
test('After first CONNECT Packet, others are queued until \'connect\' event', function (t) {
t.plan(2)
const queueLimit = 50
const broker = aedes({ queueLimit })
t.teardown(broker.close.bind(broker))
const publishP = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false
}
const connectP = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'abcde',
keepalive: 0
}
const s = setup(broker)
s.inStream.write(connectP)
process.once('warning', e => t.fail('Memory leak detected'))
for (let i = 0; i < queueLimit; i++) {
s.inStream.write(publishP)
}
broker.on('client', function (client) {
t.equal(client._parser._queue.length, queueLimit, 'Packets have been queued')
client.once('connected', () => {
t.equal(client._parser._queue, null, 'Queue is empty')
s.conn.destroy()
})
})
})
test('Test queue limit', function (t) {
t.plan(1)
const queueLimit = 50
const broker = aedes({ queueLimit })
t.teardown(broker.close.bind(broker))
const publishP = {
cmd: 'publish',
topic: 'hello',
payload: Buffer.from('world'),
qos: 0,
retain: false
}
const connectP = {
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'abcde',
keepalive: 0
}
const s = setup(broker)
s.inStream.write(connectP)
process.once('warning', e => t.fail('Memory leak detected'))
for (let i = 0; i < queueLimit + 1; i++) {
s.inStream.write(publishP)
}
broker.on('connectionError', function (conn, err) {
t.equal(err.message, 'Client queue limit reached', 'Queue error is thrown')
s.conn.destroy()
})
})
;[['fail with no error msg', 3, null, false], ['succeed with no error msg', 9, null, true], ['fail with error msg', 6, new Error('connection banned'), false], ['succeed with error msg', 6, new Error('connection banned'), true]].forEach(function (ele, idx) {
const title = ele[0]
const plan = ele[1]
const err = ele[2]
const ok = ele[3]
test('preConnect handler - ' + title, function (t) {
t.plan(plan)
const broker = aedes({
preConnect: function (client, packet, done) {
t.ok(client.connecting)
t.notOk(client.connected)
t.equal(client.version, null)
return done(err, ok)
}
})
t.teardown(broker.close.bind(broker))
const s = setup(broker)
s.inStream.write({
cmd: 'connect',
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
clientId: 'my-client-' + idx,
keepalive: 0
})
broker.on('client', function (client) {
if (ok && !err) {
t.ok(client.connecting)
t.notOk(client.connected)
t.pass('register client ok')
} else {
t.fail('no reach here')
}
})
broker.on('clientReady', function (client) {
t.notOk(client.connecting)
t.ok(client.connected)
t.pass('connect ok')
})
broker.on('clientError', function (client, err) {
t.fail('no client error')
})
broker.on('connectionError', function (client, err) {
if (err) {
t.notOk(client.connecting)
t.notOk(client.connected)
t.equal(err.message, 'connection banned')
} else {
t.fail('no connection error')
}
})
})
})
// websocket-stream based connections
test('websocket clients have access to the request object', function (t) {
t.plan(3)
const port = 4883
const broker = aedes()
broker.on('client', function (client) {
if (client.req) {
t.pass('client request object present')
if (client.req.headers) {
t.equal('sample', client.req.headers['x-test-protocol'])
}
} else {
t.fail('no request object present')
}
})
const server = http.createServer()
ws.createServer({
server
}, broker.handle)
server.listen(port, function (err) {
t.error(err, 'no error')
})
const client = mqtt.connect(`ws://localhost:${port}`, {
wsOptions: {
headers: {
'X-Test-Protocol': 'sample'
}
}
})
t.teardown(() => {
client.end(true)
broker.close()
server.close()
})
})