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 ]))