You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1007 lines
22 KiB

var test = require('tape')
, mqtt = require('./')
function testParseGenerate(name, object, buffer, opts) {
test(name + ' parse', function(t) {
t.plan(2)
var parser = mqtt.parser(opts)
, expected = object
, fixture = buffer
parser.on('packet', function(packet) {
if (packet.cmd !== 'publish') {
delete packet.topic
delete packet.payload
}
t.deepEqual(packet, expected, 'expected packet')
})
t.equal(parser.parse(fixture), 0, 'remaining bytes')
})
test(name + ' generate', function(t) {
t.equal(mqtt.generate(object).toString('hex'), buffer.toString('hex'))
t.end()
})
test(name + ' mirror', function(t) {
t.plan(2)
var parser = mqtt.parser(opts)
, expected = object
, fixture = mqtt.generate(object)
parser.on('packet', function(packet) {
if (packet.cmd !== 'publish') {
delete packet.topic
delete packet.payload
}
t.deepEqual(packet, expected, 'expected packet')
})
t.equal(parser.parse(fixture), 0, 'remaining bytes')
})
}
function testParseError(expected, fixture) {
test(expected, function(t) {
t.plan(1)
var parser = mqtt.parser()
parser.on('error', function(err) {
t.equal(err.message, expected, 'expected error message')
})
parser.on('packet', function(p) {
console.log(p)
t.fail('parse errors should not be followed by packet events')
})
parser.parse(fixture)
})
}
function testGenerateError(expected, fixture) {
test(expected, function(t) {
t.plan(1)
try {
mqtt.generate(fixture)
} catch(err) {
t.equal(expected, err.message)
}
})
}
function testParseGenerateDefaults(name, object, buffer, opts) {
test(name + ' parse', function(t) {
var parser = mqtt.parser(opts)
, expected = object
, fixture = buffer
t.plan(1 + Object.keys(expected).length)
parser.on('packet', function(packet) {
Object.keys(expected).forEach(function(key) {
t.deepEqual(packet[key], expected[key], 'expected packet property ' + key)
})
})
t.equal(parser.parse(fixture), 0, 'remaining bytes')
})
test(name + ' generate', function(t) {
t.equal(mqtt.generate(object).toString('hex'), buffer.toString('hex'))
t.end()
})
}
testParseGenerate('minimal connect', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 18
, protocolId: 'MQIsdp'
, protocolVersion: 3
, clean: false
, keepalive: 30
, clientId: 'test'
}, new Buffer([
16, 18, // Header
0, 6, // Protocol id length
77, 81, 73, 115, 100, 112, // Protocol id
3, // Protocol version
0, // Connect flags
0, 30, // Keepalive
0, 4, //Client id length
116, 101, 115, 116 // Client id
]))
testParseGenerate('no clientId with 3.1.1', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 12
, protocolId: 'MQTT'
, protocolVersion: 4
, clean: true
, keepalive: 30
, clientId: ''
}, new Buffer([
16, 12, // Header
0, 4, // Protocol id length
77, 81, 84, 84, // Protocol id
4, // Protocol version
2, // Connect flags
0, 30, // Keepalive
0, 0, //Client id length
]))
testParseGenerateDefaults('default connect', {
cmd: 'connect'
, clientId: 'test'
}, new Buffer([
16, 16, 0, 4, 77, 81, 84,
84, 4, 2, 0, 0,
0, 4, 116, 101, 115, 116
]))
testParseGenerate('empty will payload', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 47
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: new Buffer(0)
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: new Buffer('password')
}, new Buffer([
16, 47, // Header
0, 6, // Protocol id length
77, 81, 73, 115, 100, 112, // Protocol id
3, // Protocol version
246, // Connect flags
0, 30, // Keepalive
0, 4, // Client id length
116, 101, 115, 116, // Client id
0, 5, // will topic length
116, 111, 112, 105, 99, // will topic
0, 0, // will payload length
// will payload
0, 8, // username length
117, 115, 101, 114, 110, 97, 109, 101, // username
0, 8, // password length
112, 97, 115, 115, 119, 111, 114, 100 //password
]))
testParseGenerate('maximal connect', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: new Buffer('payload')
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: new Buffer('password')
}, new Buffer([
16, 54, // Header
0, 6, // Protocol id length
77, 81, 73, 115, 100, 112, // Protocol id
3, // Protocol version
246, // Connect flags
0, 30, // Keepalive
0, 4, // Client id length
116, 101, 115, 116, // Client id
0, 5, // will topic length
116, 111, 112, 105, 99, // will topic
0, 7, // will payload length
112, 97, 121, 108, 111, 97, 100, // will payload
0, 8, // username length
117, 115, 101, 114, 110, 97, 109, 101, // username
0, 8, // password length
112, 97, 115, 115, 119, 111, 114, 100 //password
]))
testParseGenerate('max connect with special chars', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 57
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'tòpic'
, payload: new Buffer('pay£oad')
}
, clean: true
, keepalive: 30
, clientId: 'te$t'
, username: 'u$ern4me'
, password: new Buffer('p4$$w0£d')
}, new Buffer([
16, 57, // Header
0, 6, // Protocol id length
77, 81, 73, 115, 100, 112, // Protocol id
3, // Protocol version
246, // Connect flags
0, 30, // Keepalive
0, 4, // Client id length
116, 101, 36, 116, // Client id
0, 6, // will topic length
116, 195, 178, 112, 105, 99, // will topic
0, 8, // will payload length
112, 97, 121, 194, 163, 111, 97, 100, // will payload
0, 8, // username length
117, 36, 101, 114, 110, 52, 109, 101, // username
0, 9, // password length
112, 52, 36, 36, 119, 48, 194, 163, 100 //password
]))
test('connect all strings generate', function(t) {
var message = {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: 'password'
}
, expected = new Buffer([
16, 54, // Header
0, 6, // Protocol id length
77, 81, 73, 115, 100, 112, // Protocol id
3, // Protocol version
246, // Connect flags
0, 30, // Keepalive
0, 4, // Client id length
116, 101, 115, 116, // Client id
0, 5, // will topic length
116, 111, 112, 105, 99, // will topic
0, 7, // will payload length
112, 97, 121, 108, 111, 97, 100, // will payload
0, 8, // username length
117, 115, 101, 114, 110, 97, 109, 101, // username
0, 8, // password length
112, 97, 115, 115, 119, 111, 114, 100 //password
])
t.equal(mqtt.generate(message).toString('hex'), expected.toString('hex'))
t.end()
})
testParseError('cannot parse protocol id', new Buffer([
16, 4,
0, 6,
77, 81
]))
testParseGenerate('connack with return code 0', {
cmd: 'connack'
, retain: false
, qos: 0
, dup: false
, length: 2
, sessionPresent: false
, returnCode: 0
}, new Buffer([
32, 2, 0, 0
]))
testParseGenerate('connack with return code 0 session present bit set', {
cmd: 'connack'
, retain: false
, qos: 0
, dup: false
, length: 2
, sessionPresent: true
, returnCode: 0
}, new Buffer([
32, 2, 1, 0
]))
testParseGenerate('connack with return code 5', {
cmd: 'connack'
, retain: false
, qos: 0
, dup: false
, length: 2
, sessionPresent: false
, returnCode: 5
}, new Buffer([
32, 2, 0, 5
]))
testParseGenerate('minimal publish', {
cmd: 'publish'
, retain: false
, qos: 0
, dup: false
, length: 10
, topic: 'test'
, payload: new Buffer('test')
}, new Buffer([
48, 10, // Header
0, 4, // Topic length
116, 101, 115, 116, // Topic (test)
116, 101, 115, 116 // Payload (test)
]))
;(function() {
var buffer = new Buffer(2048)
testParseGenerate('2KB publish packet', {
cmd: 'publish'
, retain: false
, qos: 0
, dup: false
, length: 2054
, topic: 'test'
, payload: buffer
}, Buffer.concat([new Buffer([
48, 134, 16, // Header
0, 4, // Topic length
116, 101, 115, 116, // Topic (test)
]), buffer]))
})()
;(function() {
var buffer = new Buffer(2 * 1024 * 1024)
testParseGenerate('2MB publish packet', {
cmd: 'publish'
, retain: false
, qos: 0
, dup: false
, length: 6 + 2 * 1024 * 1024
, topic: 'test'
, payload: buffer
}, Buffer.concat([new Buffer([
48, 134, 128, 128, 1, // Header
0, 4, // Topic length
116, 101, 115, 116, // Topic (test)
]), buffer]))
})()
testParseGenerate('maximal publish', {
cmd:'publish'
, retain: true
, qos: 2
, length: 12
, dup: true
, topic: 'test'
, messageId: 10
, payload: new Buffer('test')
}, new Buffer([
61, 12, // Header
0, 4, // Topic length
116, 101, 115, 116, // Topic
0, 10, // Message id
116, 101, 115, 116 // Payload
]))
test('publish all strings generate', function(t) {
var message = {
cmd:'publish'
, retain: true
, qos: 2
, length: 12
, dup: true
, topic: 'test'
, messageId: 10
, payload: new Buffer('test')
}
, expected = new Buffer([
61, 12, // Header
0, 4, // Topic length
116, 101, 115, 116, // Topic
0, 10, // Message id
116, 101, 115, 116 // Payload
])
t.equal(mqtt.generate(message).toString('hex'), expected.toString('hex'))
t.end()
})
testParseGenerate('empty publish', {
cmd: 'publish'
, retain: false
, qos: 0
, dup: false
, length: 6
, topic: 'test'
, payload: new Buffer(0)
}, new Buffer([
48, 6, // Header
0, 4, // Topic length
116, 101, 115, 116 // Topic
// Empty payload
]))
test('splitted publish parse', function(t) {
t.plan(3)
var parser = mqtt.parser()
, rest
, expected = {
cmd: 'publish'
, retain: false
, qos: 0
, dup: false
, length: 10
, topic: 'test'
, payload: new Buffer('test')
};
parser.on('packet', function(packet) {
t.deepEqual(packet, expected, 'expected packet')
})
t.equal(parser.parse(new Buffer([
48, 10, // Header
0, 4, // Topic length
116, 101, 115, 116 // Topic (test)
])), 6, 'remaining bytes')
t.equal(parser.parse(new Buffer([
116, 101, 115, 116 // Payload (test)
])), 0, 'remaining bytes')
})
testParseGenerate('puback', {
cmd: 'puback'
, retain: false
, qos: 0
, dup: false
, length: 2
, messageId: 2
}, new Buffer([
64, 2, // Header
0, 2 // Message id
]))
testParseGenerate('pubrec', {
cmd: 'pubrec'
, retain: false
, qos: 0
, dup: false
, length: 2
, messageId: 2
}, new Buffer([
80, 2, // Header
0, 2 // Message id
]))
testParseGenerate('pubrel', {
cmd: 'pubrel'
, retain: false
, qos: 1
, dup: false
, length: 2
, messageId: 2
}, new Buffer([
98, 2, // Header
0, 2 // Message id
]))
testParseGenerate('pubcomp', {
cmd: 'pubcomp'
, retain: false
, qos: 0
, dup: false
, length: 2
, messageId: 2
}, new Buffer([
112, 2, // Header
0, 2 // Message id
]))
testParseError('wrong subscribe header', new Buffer([
128, 9, // Header (subscribe, qos=0, length=9)
0, 6, // message id (6)
0, 4, // topic length,
116, 101, 115, 116, // Topic (test)
0 // qos (0)
]))
testParseGenerate('subscribe to one topic', {
cmd: 'subscribe'
, retain: false
, qos: 1
, dup: false
, length: 9
, subscriptions: [
{
topic: "test"
, qos: 0
}
]
, messageId: 6
}, new Buffer([
130, 9, // Header (subscribe, qos=1, length=9)
0, 6, // message id (6)
0, 4, // topic length,
116, 101, 115, 116, // Topic (test)
0 // qos (0)
]))
testParseGenerate('subscribe to three topics', {
cmd: 'subscribe'
, retain: false
, qos: 1
, dup: false
, length: 23
, subscriptions: [
{
topic: "test"
, qos: 0
},{
topic: "uest"
, qos: 1
},{
topic: "tfst"
, qos: 2
}
],
messageId: 6
}, new Buffer([
130, 23, // Header (publish, qos=1, length=9)
0, 6, // message id (6)
0, 4, // topic length,
116, 101, 115, 116, // Topic (test)
0, // qos (0)
0, 4, // topic length
117, 101, 115, 116, // Topic (uest)
1, // qos (1)
0, 4, // topic length
116, 102, 115, 116, // Topic (tfst)
2 // qos (2)
]))
testParseGenerate('suback', {
cmd: 'suback'
, retain: false
, qos: 0
, dup: false
, length: 6
, granted: [0, 1, 2, 128]
, messageId: 6
}, new Buffer([
144, 6, // Header
0, 6, // Message id
0, 1, 2, 128 // Granted qos (0, 1, 2) and a rejected being 0x80
]))
testParseGenerate('unsubscribe', {
cmd: 'unsubscribe'
, retain: false
, qos: 1
, dup: false
, length: 14
, unsubscriptions: [
'tfst'
, 'test'
],
messageId: 7
}, new Buffer([
162, 14,
0, 7, // message id (7)
0, 4, // topic length
116, 102, 115, 116, // Topic (tfst)
0, 4, // topic length,
116, 101, 115, 116, // Topic (test)
]))
testParseGenerate('unsuback', {
cmd: 'unsuback'
, retain: false
, qos: 0
, dup: false
, length: 2
, messageId: 8
}, new Buffer([
176, 2, // Header
0, 8 // Message id
]))
testParseGenerate('pingreq', {
cmd: 'pingreq'
, retain: false
, qos: 0
, dup: false
, length: 0
}, new Buffer([
192, 0 // Header
]))
testParseGenerate('pingresp', {
cmd: 'pingresp'
, retain: false
, qos: 0
, dup: false
, length: 0
}, new Buffer([
208, 0 // Header
]))
testParseGenerate('disconnect', {
cmd: 'disconnect'
, retain: false
, qos: 0
, dup: false
, length: 0
}, new Buffer([
224, 0 // Header
]))
testGenerateError('unknown command', {})
testGenerateError('Invalid protocol id', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 42
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: 'password'
})
testGenerateError('clientId must be supplied before 3.1.1', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: true
, keepalive: 30
, username: 'username'
, password: 'password'
})
testGenerateError('clientId must be given if cleanSession set to 0', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQTT'
, protocolVersion: 4
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: false
, keepalive: 30
, username: 'username'
, password: 'password'
})
testGenerateError('Invalid keepalive', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: true
, keepalive: 'hello'
, clientId: 'test'
, username: 'username'
, password: 'password'
})
testGenerateError('Invalid will', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: 42
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: 'password'
})
testGenerateError('Invalid will topic', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, payload: 'payload'
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: 'password'
})
testGenerateError('Invalid will payload', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 42
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: 'password'
})
testGenerateError('Invalid username', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 42
, password: 'password'
})
testGenerateError('Invalid password', {
cmd: 'connect'
, retain: false
, qos: 0
, dup: false
, length: 54
, protocolId: 'MQIsdp'
, protocolVersion: 3
, will: {
retain: true
, qos: 2
, topic: 'topic'
, payload: 'payload'
}
, clean: true
, keepalive: 30
, clientId: 'test'
, username: 'username'
, password: 42
})
// the following test case was designed after experiencing errors
// when trying to connect with tls on a non tls mqtt port
// the specific behaviour is:
// - first byte suggests this is a connect message
// - second byte suggests message length to be smaller than buffer length
// thus payload processing starts
// - the first two bytes suggest a protocol identifier string length
// that leads the parser pointer close to the end of the buffer
// - when trying to read further connect flags the buffer produces
// a "out of range" Error
//
testParseError('packet too short', new Buffer([
16, 9,
0, 6,
77, 81, 73, 115, 100, 112,
3
]))
// CONNECT Packets that show other protocol ids than
// the valid values MQTT and MQIsdp should cause an error
// those packets are a hint that this is not a mqtt connection
testParseError('invalid protocol id', new Buffer([
16, 18,
0, 6,
65,65,65,65,65,65, // AAAAAA
3, // protocol version
0, // connect flags
0, 10, // keep alive
0, 4, //Client id length
116, 101, 115, 116 // Client id
]))
// CONNECT Packets that contain an unsupported protocol version
// flag (i.e. not `3` or `4`) should cause an error
testParseError('invalid protocol version', new Buffer([
16, 18,
0, 6,
77, 81, 73, 115, 100, 112, // Protocol id
1, // protocol version
0, // connect flags
0, 10, // keep alive
0, 4, //Client id length
116, 101, 115, 116 // Client id
]))
// when a packet contains a string in the variable header and the
// given string length of this exceeds the overall length of the packet that
// was specified in the fixed header, parsing must fail.
// this case simulates this behavior with the protocol id string of the
// CONNECT packet. The fixed header suggests a remaining length of 8 bytes
// which would be exceeded by the string length of 15
// in this case, a protocol id parse error is expected
testParseError('cannot parse protocol id', new Buffer([
16, 8, // fixed header
0, 15, // string length 15 --> 15 > 8 --> error!
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112,
77, 81, 73, 115, 100, 112
]))
test('stops parsing after first error', function(t) {
t.plan(4)
var parser = mqtt.parser()
var packetCount = 0
var errorCount = 0
var expectedPackets = 1
var expectedErrors = 1
parser.on('packet', function(packet) {
t.ok(++packetCount <= expectedPackets, 'expected <= ' + expectedPackets + ' packets')
})
parser.on('error', function(err) {
t.ok(++errorCount <= expectedErrors, 'expected <= ' + expectedErrors + ' errors')
})
parser.parse(new Buffer([
// first, a valid connect packet:
16, 12, // Header
0, 4, // Protocol id length
77, 81, 84, 84, // Protocol id
4, // Protocol version
2, // Connect flags
0, 30, // Keepalive
0, 0, //Client id length
// then an invalid subscribe packet:
128, 9, // Header (subscribe, qos=0, length=9)
0, 6, // message id (6)
0, 4, // topic length,
116, 101, 115, 116, // Topic (test)
0, // qos (0)
// and another invalid subscribe packet:
128, 9, // Header (subscribe, qos=0, length=9)
0, 6, // message id (6)
0, 4, // topic length,
116, 101, 115, 116, // Topic (test)
0, // qos (0)
// finally, a valid disconnect packet:
224, 0, // Header
]))
// calling parse again clears the error and continues parsing
packetCount = 0
errorCount = 0
expectedPackets = 2
expectedErrors = 0
parser.parse(new Buffer([
// connect:
16, 12, // Header
0, 4, // Protocol id length
77, 81, 84, 84, // Protocol id
4, // Protocol version
2, // Connect flags
0, 30, // Keepalive
0, 0, //Client id length
// disconnect:
224, 0, // Header
]))
})
// When a Subscribe packet contains a topic_filter and the given
// length is topic_filter.length + 1 then the last byte (requested QoS) is interpreted as topic_filter
// reading the requested_qos at the end causes 'Index out of range' read
testParseError('Malformed Subscribe Payload', Buffer.from([
130, 14, // subscribe header and remaining length
0, 123, // packet ID
0, 10, // topic filter length
104, 105, 106, 107, 108, 47, 109, 110, 111, // topic filter with length of 9 bytes
0 // requested QoS
]))