From 9575afbca91bd6369bf0ba52f4ccb76eb46f3cc0 Mon Sep 17 00:00:00 2001 From: Alexander Kiranov Date: Sat, 25 Nov 2023 17:34:02 +0200 Subject: [PATCH] protocol5: added parsing for all packet types --- mqtt/const.lua | 4 +- mqtt/protocol5.lua | 730 +++++++++++++++++++++++---------- tests/spec/protocol5-make.lua | 78 +++- tests/spec/protocol5-parse.lua | 379 ++++++++++++++++- 4 files changed, 967 insertions(+), 224 deletions(-) diff --git a/mqtt/const.lua b/mqtt/const.lua index 111230e..ff2a0af 100644 --- a/mqtt/const.lua +++ b/mqtt/const.lua @@ -14,4 +14,6 @@ local const = { _VERSION = "3.4.3", } -return const \ No newline at end of file +return const + +-- vim: ts=4 sts=4 sw=4 noet ft=lua diff --git a/mqtt/protocol5.lua b/mqtt/protocol5.lua index 8510aa4..43d5797 100644 --- a/mqtt/protocol5.lua +++ b/mqtt/protocol5.lua @@ -26,6 +26,9 @@ local string = require("string") local str_char = string.char local fmt = string.format +local const = require("mqtt.const") +local const_v50 = const.v50 + local tools = require("mqtt.tools") local sortedpairs = tools.sortedpairs @@ -60,6 +63,7 @@ local packet_type = protocol.packet_type local packet_mt = protocol.packet_mt local connack_packet_mt = protocol.connack_packet_mt local start_parse_packet = protocol.start_parse_packet +local parse_packet_connect_input = protocol.parse_packet_connect_input -- Returns true if given value is a valid Retain Handling option, DOC: 3.8.3.1 Subscription Options local function check_retain_handling(val) @@ -285,7 +289,7 @@ local allowed_properties = { [0x26] = true, -- DOC: 3.7.2.2.3 User Property }, [packet_type.SUBSCRIBE] = { - [0x0B] = true, -- DOC: 3.8.2.1.2 Subscription Identifier + [0x0B] = { multiple=false }, -- DOC: 3.8.2.1.2 Subscription Identifier -- DOC: It is a Protocol Error to include the Subscription Identifier more than once. [0x26] = true, -- DOC: 3.8.2.1.3 User Property }, [packet_type.SUBACK] = { @@ -691,28 +695,32 @@ function protocol5.make_packet(args) assert(type(args) == "table", "expecting args to be a table") assert(type(args.type) == "number", "expecting .type number in args") local ptype = args.type - if ptype == packet_type.CONNECT then + if ptype == packet_type.CONNECT then -- 1 return make_packet_connect(args) - elseif ptype == packet_type.PUBLISH then + -- TODO: CONNACK + elseif ptype == packet_type.PUBLISH then -- 3 return make_packet_publish(args) - elseif ptype == packet_type.PUBACK then + elseif ptype == packet_type.PUBACK then -- 4 return make_packet_puback(args) - elseif ptype == packet_type.PUBREC then + elseif ptype == packet_type.PUBREC then -- 5 return make_packet_pubrec(args) - elseif ptype == packet_type.PUBREL then + elseif ptype == packet_type.PUBREL then -- 6 return make_packet_pubrel(args) - elseif ptype == packet_type.PUBCOMP then + elseif ptype == packet_type.PUBCOMP then -- 7 return make_packet_pubcomp(args) - elseif ptype == packet_type.SUBSCRIBE then + elseif ptype == packet_type.SUBSCRIBE then -- 8 return make_packet_subscribe(args) - elseif ptype == packet_type.UNSUBSCRIBE then + -- TODO: SUBACK + elseif ptype == packet_type.UNSUBSCRIBE then -- 10 return make_packet_unsubscribe(args) - elseif ptype == packet_type.PINGREQ then + -- TODO: UNSUBACK + elseif ptype == packet_type.PINGREQ then -- 12 -- DOC: 3.12 PINGREQ – PING request return combine("\192\000") -- 192 == 0xC0, type == 12, flags == 0 - elseif ptype == packet_type.DISCONNECT then + -- TODO: PINGRESP + elseif ptype == packet_type.DISCONNECT then -- 14 return make_packet_disconnect(args) - elseif ptype == packet_type.AUTH then + elseif ptype == packet_type.AUTH then -- 15 return make_packet_auth(args) else error("unexpected packet type to make: "..ptype) @@ -790,9 +798,15 @@ local function parse_properties(ptype, read_data, input, packet) local value value, err = property_parse[prop_id](read_data) if err then - return false, "failed ro parse property "..prop_id.." value: "..err + return false, "failed to parse property "..prop_id.." value: "..err end - if property_multiple[prop_id] then + if allowed[prop_id] ~= true then + if packet.properties[properties[prop_id]] ~= nil then + return false, "it is a Protocol Error to include the "..properties[prop_id].." ("..prop_id..") property more than once" + end + end + -- make an array of property values, if it's allowed to send multiple such properties + if allowed[prop_id] == true and property_multiple[prop_id] then local curr = packet.properties[properties[prop_id]] or {} curr[#curr + 1] = value packet.properties[properties[prop_id]] = curr @@ -804,236 +818,534 @@ local function parse_properties(ptype, read_data, input, packet) return true end --- Parse packet using given read_func --- Returns packet on success or false and error message on failure -function protocol5.parse_packet(read_func) - local ptype, flags, input = start_parse_packet(read_func) - if not ptype then - return false, flags +-- Parse CONNACK packet, DOC: 3.2 CONNACK – Connect acknowledgement +local function parse_packet_connack(ptype, flags, input) + -- DOC: 3.2.1 CONNACK Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + if input.available < 3 then + return false, packet_type[ptype]..": expecting data of length 3 bytes or more" end - local byte1, byte2, err, rc, ok, packet, topic, packet_id local read_data = input.read_func + -- DOC: 3.2.2 CONNACK Variable Header + -- DOC: 3.2.2.1.1 Session Present + -- DOC: 3.2.2.2 Connect Reason Code + local byte1, byte2 = parse_uint8(read_data), parse_uint8(read_data) + local sp = (band(byte1, 0x1) ~= 0) + local packet = setmetatable({type=ptype, sp=sp, rc=byte2}, connack_packet_mt) + -- DOC: 3.2.2.3 CONNACK Properties + local ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err + end + return packet +end - -- parse read data according type in fixed header - if ptype == packet_type.CONNACK then - -- DOC: 3.2 CONNACK – Connect acknowledgement - if input.available < 3 then - return false, "expecting data of length 3 bytes or more" +-- Parse PUBLISH packet, DOC: 3.3 PUBLISH – Publish message +local function parse_packet_publish(ptype, flags, input) + -- DOC: 3.3.1 PUBLISH Fixed Header + -- DOC: 3.3.1.1 DUP + local dup = (band(flags, 0x8) ~= 0) + -- DOC: 3.3.1.2 QoS + local qos = band(rshift(flags, 1), 0x3) + -- DOC: 3.3.1.3 RETAIN + local retain = (band(flags, 0x1) ~= 0) + -- DOC: 3.3.2 PUBLISH Variable Header + -- DOC: 3.3.2.1 Topic Name + local read_data = input.read_func + local topic, err = parse_string(read_data) + if not topic then + return false, packet_type[ptype]..": failed to parse topic: "..err + end + -- DOC: 3.3.2.2 Packet Identifier + local packet_id, ok + if qos > 0 then + packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + end + -- DOC: 3.3.2.3 PUBLISH Properties + local packet = setmetatable({type=ptype, dup=dup, qos=qos, retain=retain, packet_id=packet_id, topic=topic}, packet_mt) + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err + end + if input.available > 0 then + -- DOC: 3.3.3 PUBLISH Payload + packet.payload = read_data(input.available) + end + return packet +end + +-- Parse PUBACK packet, DOC: 3.4 PUBACK – Publish acknowledgement +local function parse_packet_puback(ptype, flags, input) + -- DOC: 3.4.1 PUBACK Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.4.2 PUBACK Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) + if input.available > 0 then + -- DOC: 3.4.2.1 PUBACK Reason Code + local rc, ok + rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse rc: "..err end - -- DOC: 3.2.2.1.1 Session Present - -- DOC: 3.2.2.2 Connect Reason Code - byte1, byte2 = parse_uint8(read_data), parse_uint8(read_data) - local sp = (band(byte1, 0x1) ~= 0) - packet = setmetatable({type=ptype, sp=sp, rc=byte2}, connack_packet_mt) - -- DOC: 3.2.2.3 CONNACK Properties + packet.rc = rc + -- DOC: 3.4.2.2 PUBACK Properties ok, err = parse_properties(ptype, read_data, input, packet) if not ok then - return false, "failed to parse packet properties: "..err - end - elseif ptype == packet_type.PUBLISH then - -- DOC: 3.3 PUBLISH – Publish message - -- DOC: 3.3.1.1 DUP - local dup = (band(flags, 0x8) ~= 0) - -- DOC: 3.3.1.2 QoS - local qos = band(rshift(flags, 1), 0x3) - -- DOC: 3.3.1.3 RETAIN - local retain = (band(flags, 0x1) ~= 0) - -- DOC: 3.3.2.1 Topic Name - topic, err = parse_string(read_data) - if not topic then - return false, "failed to parse topic: "..err + return false, packet_type[ptype]..": failed to parse packet properties: "..err end - -- DOC: 3.3.2.2 Packet Identifier - if qos > 0 then - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err - end + end + return packet +end + +-- Parse PUBREC packet, DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1) +local function parse_packet_pubrec(ptype, flags, input) + -- DOC: 3.5.1 PUBREC Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.5.2 PUBREC Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) + if input.available > 0 then + -- DOC: 3.5.2.1 PUBREC Reason Code + local rc + rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse rc: "..err end - -- DOC: 3.3.2.3 PUBLISH Properties - packet = setmetatable({type=ptype, dup=dup, qos=qos, retain=retain, packet_id=packet_id, topic=topic}, packet_mt) + packet.rc = rc + -- DOC: 3.5.2.2 PUBREC Properties + local ok ok, err = parse_properties(ptype, read_data, input, packet) if not ok then - return false, "failed to parse packet properties: "..err + return false, packet_type[ptype]..": failed to parse packet properties: "..err end - if input.available > 0 then - -- DOC: 3.3.3 PUBLISH Payload - packet.payload = read_data(input.available) + end + return packet +end + +-- Parse PUBREL packet, DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2) +local function parse_packet_pubrel(ptype, flags, input) + -- DOC: 3.6.1 PUBREL Fixed Header + if flags ~= 2 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.6.2 PUBREL Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) + if input.available > 0 then + -- DOC: 3.6.2.1 PUBREL Reason Code + local rc + rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse rc: "..err end - elseif ptype == packet_type.PUBACK then - -- DOC: 3.4 PUBACK – Publish acknowledgement - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err + packet.rc = rc + -- DOC: 3.6.2.2 PUBREL Properties + local ok + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err end - packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) - if input.available > 0 then - -- DOC: 3.4.2.1 PUBACK Reason Code - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse rc: "..err - end - packet.rc = rc - -- DOC: 3.4.2.2 PUBACK Properties - ok, err = parse_properties(ptype, read_data, input, packet) - if not ok then - return false, "failed to parse packet properties: "..err - end + end + return packet +end + +-- Parse PUBCOMP packet, DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3) +local function parse_packet_pubcomp(ptype, flags, input) + -- DOC: 3.7.1 PUBCOMP Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.7.2 PUBCOMP Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) + if input.available > 0 then + -- DOC: 3.7.2.1 PUBCOMP Reason Code + local rc + rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse rc: "..err end - elseif ptype == packet_type.PUBREC then - -- DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1) - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err + packet.rc = rc + -- DOC: 3.7.2.2 PUBCOMP Properties + local ok + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err end - packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) - if input.available > 0 then - -- DOC: 3.5.2.1 PUBREC Reason Code - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse rc: "..err - end - packet.rc = rc - -- DOC: 3.5.2.2 PUBREC Properties - ok, err = parse_properties(ptype, read_data, input, packet) - if not ok then - return false, "failed to parse packet properties: "..err - end + end + return packet +end + +-- Parse SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe request +local function parse_packet_subscribe(ptype, flags, input) + -- DOC: 3.8.1 SUBSCRIBE Fixed Header + if flags ~= 2 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.8.2 SUBSCRIBE Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + local packet = setmetatable({type=ptype, packet_id=packet_id, properties={}, user_properties={}}, packet_mt) + -- DOC: 3.8.2.1 SUBSCRIBE Properties + local ok + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err + end + -- DOC: 3.8.3 SUBSCRIBE Payload + if input.available == 0 then + -- DOC: A SUBSCRIBE packet with no Payload is a Protocol Error. + return false, packet_type[ptype]..": empty subscriptions list" + end + local subscriptions = {} + while input.available > 0 do + local topic_filter + topic_filter, err = parse_string(input.read_func) + if not topic_filter then + return false, packet_type[ptype]..": failed to parse SUBSCRIBE topic filter: "..err end - elseif ptype == packet_type.PUBREL then - -- DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2) - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err + -- DOC: 3.8.3.1 Subscription Options + local subscription_options + subscription_options, err = parse_uint8(input.read_func) + if not subscription_options then + return false, packet_type[ptype]..": failed to parse subscription_options: "..err end - packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) - if input.available > 0 then - -- DOC: 3.6.2.1 PUBREL Reason Code - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse rc: "..err - end - packet.rc = rc - -- DOC: 3.6.2.2 PUBREL Properties - ok, err = parse_properties(ptype, read_data, input, packet) - if not ok then - return false, "failed to parse packet properties: "..err - end + subscriptions[#subscriptions + 1] = { + topic = topic_filter, + qos = band(subscription_options, 0x3), + no_local = band(subscription_options, 0x4) ~= 0, + retain_as_published = band(subscription_options, 0x8) ~= 0, -- Retain As Published + retain_handling = band(rshift(subscription_options, 4), 0x3), -- Retain Handling + } + end + packet.subscriptions = subscriptions + return packet +end + +-- Parse SUBACK packet, DOC: 3.9 SUBACK – Subscribe acknowledgement +local function parse_packet_suback(ptype, flags, input) + -- DOC: 3.9.1 SUBACK Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.9.2 SUBACK Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + -- DOC: 3.9.2.1 SUBACK Properties + local packet = setmetatable({type=ptype, packet_id=packet_id}, packet_mt) + local ok + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err + end + -- DOC: 3.9.3 SUBACK Payload + local rcs = {} + while input.available > 0 do + local rc + rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse reason code: "..err end - elseif ptype == packet_type.PUBCOMP then - -- DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3) - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err + rcs[#rcs + 1] = rc + end + if not next(rcs) then + return false, packet_type[ptype]..": expecting at least one reason code" + end + packet.rc = rcs -- TODO: reason codes table somewhere should be placed? + return packet +end + +-- Parse UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE – Unsubscribe request +local function parse_packet_unsubscribe(ptype, flags, input) + -- DOC: 3.10.1 UNSUBSCRIBE Fixed Header + if flags ~= 2 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.10.2 UNSUBSCRIBE Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + local packet = setmetatable({type=ptype, packet_id=packet_id, properties={}, user_properties={}}, packet_mt) + -- DOC: 3.10.2.1 UNSUBSCRIBE Properties + local ok + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err + end + -- 3.10.3 UNSUBSCRIBE Payload + local subscriptions = {} + while input.available > 0 do + local topic_filter + topic_filter, err = parse_string(input.read_func) + if not topic_filter then + return false, packet_type[ptype]..": failed to parse topic filter: "..err end - packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt) - if input.available > 0 then - -- DOC: 3.7.2.1 PUBCOMP Reason Code - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse rc: "..err - end - packet.rc = rc - -- DOC: 3.7.2.2 PUBCOMP Properties - ok, err = parse_properties(ptype, read_data, input, packet) - if not ok then - return false, "failed to parse packet properties: "..err - end + subscriptions[#subscriptions + 1] = topic_filter + end + packet.subscriptions = subscriptions + return packet +end + +-- Parse UNSUBACK packet, DOC: 3.11 UNSUBACK – Unsubscribe acknowledgement +local function parse_packet_unsuback(ptype, flags, input) + -- DOC: 3.11.1 UNSUBACK Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.11.2 UNSUBACK Variable Header + local packet_id, err = parse_uint16(read_data) + if not packet_id then + return false, packet_type[ptype]..": failed to parse packet_id: "..err + end + -- 3.11.2.1 UNSUBACK Properties + local packet = setmetatable({type=ptype, packet_id=packet_id}, packet_mt) + local ok + ok, err = parse_properties(ptype, read_data, input, packet) + if not ok then + return false, packet_type[ptype]..": failed to parse packet properties: "..err + end + -- 3.11.3 UNSUBACK Payload + local rcs = {} + while input.available > 0 do + local rc + rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse reason code: "..err end - elseif ptype == packet_type.SUBACK then - -- DOC: 3.9 SUBACK – Subscribe acknowledgement - -- DOC: 3.9.2 SUBACK Variable Header - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err + rcs[#rcs + 1] = rc + end + if not next(rcs) then + return false, packet_type[ptype]..": expecting at least one reason code in" + end + packet.rc = rcs + return packet +end + +-- Parse PINGREQ packet, DOC: 3.12 PINGREQ – PING request +local function parse_packet_pingreq(ptype, flags, input_) + -- DOC: 3.12.1 PINGREQ Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + return setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt) +end + +-- Parse PINGRESP packet, DOC: 3.13 PINGRESP – PING response +local function parse_packet_pingresp(ptype, flags, input_) + -- DOC: 3.13.1 PINGRESP Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + return setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt) +end + +-- Parse DISCONNECT packet, DOC: 3.14 DISCONNECT – Disconnect notification +local function parse_packet_disconnect(ptype, flags, input) + -- DOC: 3.14.1 DISCONNECT Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + local packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, packet_mt) + if input.available > 0 then + -- DOC: 3.14.2 DISCONNECT Variable Header + -- DOC: 3.14.2.1 Disconnect Reason Code + local rc, err = parse_uint8(read_data) -- TODO: reason codes table? + if not rc then + return false, packet_type[ptype]..": failed to parse rc: "..err end - -- DOC: 3.9.2.1 SUBACK Properties - packet = setmetatable({type=ptype, packet_id=packet_id}, packet_mt) + packet.rc = rc + -- DOC: 3.14.2.2 DISCONNECT Properties + local ok ok, err = parse_properties(ptype, read_data, input, packet) if not ok then - return false, "failed to parse packet properties: "..err + return false, packet_type[ptype]..": failed to parse packet properties: "..err end - -- DOC: 3.9.3 SUBACK Payload - local rcs = {} - while input.available > 0 do - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse reason code: "..err - end - rcs[#rcs + 1] = rc - end - if not next(rcs) then - return false, "expecting at least one reason code in SUBACK" - end - packet.rc = rcs -- TODO: reason codes table somewhere should be placed - elseif ptype == packet_type.UNSUBACK then - -- DOC: 3.11 UNSUBACK – Unsubscribe acknowledgement - -- DOC: 3.11.2 UNSUBACK Variable Header - packet_id, err = parse_uint16(read_data) - if not packet_id then - return false, "failed to parse packet_id: "..err + end + return packet +end + +-- Parse AUTH packet, DOC: 3.15 AUTH – Authentication exchange +local function parse_packet_auth(ptype, flags, input) + -- DOC: 3.15.1 AUTH Fixed Header + if flags ~= 0 then -- Reserved + return false, packet_type[ptype]..": unexpected flags value: "..flags + end + local read_data = input.read_func + -- DOC: 3.15.2.1 Authenticate Reason Code + local packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, packet_mt) + if input.available > 1 then + -- DOC: 3.15.2 AUTH Variable Header + local rc, err = parse_uint8(read_data) + if not rc then + return false, packet_type[ptype]..": failed to parse Authenticate Reason Code: "..err end - -- 3.11.2.1 UNSUBACK Properties - packet = setmetatable({type=ptype, packet_id=packet_id}, packet_mt) + packet.rc = rc + -- DOC: 3.15.2.2 AUTH Properties + local ok ok, err = parse_properties(ptype, read_data, input, packet) if not ok then - return false, "failed to parse packet properties: "..err + return false, packet_type[ptype]..": failed to parse packet properties: "..err end - -- 3.11.3 UNSUBACK Payload - local rcs = {} - while input.available > 0 do - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse reason code: "..err - end - rcs[#rcs + 1] = rc + end + return packet +end + +-- Parse packet using given read_func +-- Returns packet on success or false and error message on failure +function protocol5.parse_packet(read_func) + local ptype, flags, input = start_parse_packet(read_func) + if not ptype then + return false, flags + end + local packet, err + + -- parse read data according type in fixed header + if ptype == packet_type.CONNECT then -- 1 + packet, err = parse_packet_connect_input(input, const_v50) + elseif ptype == packet_type.CONNACK then -- 2 + packet, err = parse_packet_connack(ptype, flags, input) + elseif ptype == packet_type.PUBLISH then -- 3 + packet, err = parse_packet_publish(ptype, flags, input) + elseif ptype == packet_type.PUBACK then -- 4 + packet, err = parse_packet_puback(ptype, flags, input) + elseif ptype == packet_type.PUBREC then -- 5 + packet, err = parse_packet_pubrec(ptype, flags, input) + elseif ptype == packet_type.PUBREL then -- 6 + packet, err = parse_packet_pubrel(ptype, flags, input) + elseif ptype == packet_type.PUBCOMP then -- 7 + packet, err = parse_packet_pubcomp(ptype, flags, input) + elseif ptype == packet_type.SUBSCRIBE then -- 8 + packet, err = parse_packet_subscribe(ptype, flags, input) + elseif ptype == packet_type.SUBACK then -- 9 + packet, err = parse_packet_suback(ptype, flags, input) + elseif ptype == packet_type.UNSUBSCRIBE then -- 10 + packet, err = parse_packet_unsubscribe(ptype, flags, input) + elseif ptype == packet_type.UNSUBACK then -- 11 + packet, err = parse_packet_unsuback(ptype, flags, input) + elseif ptype == packet_type.PINGREQ then -- 12 + packet, err = parse_packet_pingreq(ptype, flags, input) + elseif ptype == packet_type.PINGRESP then -- 13 + packet, err = parse_packet_pingresp(ptype, flags, input) + elseif ptype == packet_type.DISCONNECT then -- 14 + packet, err = parse_packet_disconnect(ptype, flags, input) + elseif ptype == packet_type.AUTH then -- 15 + packet, err = parse_packet_auth(ptype, flags, input) + else + return false, "unexpected packet type received: "..tostring(ptype) + end + if packet and input.available > 0 then + return false, packet_type[ptype]..": extra data in remaining length left after packet parsing" + end + return packet, err +end + +-- Continue parsing of the MQTT v5.0 CONNECT packet +-- Internally called from the protocol.parse_packet_connect_input() function +-- Returns packet on success or false and error message on failure +function protocol5._parse_packet_connect_continue(input, packet) + local read_func = input.read_func + local ok, client_id, err + + -- DOC: 3.1.2.11 CONNECT Properties + ok, err = parse_properties(packet_type.CONNECT, read_func, input, packet) + if not ok then + return false, "CONNECT: failed to parse packet properties: "..err + end + + -- DOC: 3.1.3 CONNECT Payload + + -- DOC: 3.1.3.1 Client Identifier (ClientID) + client_id, err = parse_string(read_func) + if not client_id then + return false, "CONNECT: failed to parse client_id: "..err + end + packet.client_id = client_id + + local will = packet.will + if will then + -- DOC: 3.1.3.2 Will Properties + ok, err = parse_properties("will", read_func, input, will) + if not ok then + return false, "CONNECT: failed to parse will message properties: "..err end - if not next(rcs) then - return false, "expecting at least one reason code in UNSUBACK" + + -- DOC: 3.1.3.3 Will Topic + local will_topic, will_payload + will_topic, err = parse_string(read_func) + if not will_topic then + return false, "CONNECT: failed to parse will_topic: "..err end - packet.rc = rcs - elseif ptype == packet_type.PINGRESP then - -- DOC: 3.13 PINGRESP – PING response - packet = setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt) - elseif ptype == packet_type.DISCONNECT then - -- DOC: 3.14 DISCONNECT – Disconnect notification - packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, packet_mt) - if input.available > 0 then - -- DOC: 3.14.2.1 Disconnect Reason Code - rc, err = parse_uint8(read_data) -- TODO: reason codes table - if not rc then - return false, "failed to parse rc: "..err - end - packet.rc = rc - -- DOC: 3.14.2.2 DISCONNECT Properties - ok, err = parse_properties(ptype, read_data, input, packet) - if not ok then - return false, "failed to parse packet properties: "..err - end + will.topic = will_topic + + -- DOC: 3.1.3.4 Will Payload + will_payload, err = parse_string(read_func) + if not will_payload then + return false, "CONNECT: failed to parse will_payload: "..err end - elseif ptype == packet_type.AUTH then - -- DOC: 3.15 AUTH – Authentication exchange - -- DOC: 3.15.2.1 Authenticate Reason Code - packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, packet_mt) - if input.available > 1 then - rc, err = parse_uint8(read_data) - if not rc then - return false, "failed to parse Authenticate Reason Code: "..err - end - packet.rc = rc - -- DOC: 3.15.2.2 AUTH Properties - ok, err = parse_properties(ptype, read_data, input, packet) - if not ok then - return false, "failed to parse packet properties: "..err - end + will.payload = will_payload + end + + -- DOC: 3.1.3.5 User Name + if packet.username then + local username + username, err = parse_string(read_func) + if not username then + return false, "CONNECT: failed to parse username: "..err end + packet.username = username else - return false, "unexpected packet type received: "..tostring(ptype) + packet.username = nil end - if input.available > 0 then - return false, "extra data in remaining length left after packet parsing" + + -- DOC: 3.1.3.6 Password + if packet.password then + local password + password, err = parse_string(read_func) + if not password then + return false, "CONNECT: failed to parse password: "..err + end + packet.password = password + else + packet.password = nil end - return packet + + return setmetatable(packet, packet_mt) end -- export module table diff --git a/tests/spec/protocol5-make.lua b/tests/spec/protocol5-make.lua index 36af773..36de60e 100644 --- a/tests/spec/protocol5-make.lua +++ b/tests/spec/protocol5-make.lua @@ -1,7 +1,7 @@ -- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/protocol5-make.lua -- DOC: https://docs.oasis-open.org/mqtt/mqtt/v5.0/cos02/mqtt-v5.0-cos02.html -describe("MQTT v5.0 protocol: making packets", function() +describe("MQTT v5.0 protocol: making packets: CONNECT[1]", function() local tools = require("mqtt.tools") local extract_hex = require("./tools/extract_hex") local protocol = require("mqtt.protocol") @@ -189,6 +189,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: PUBLISH[3]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("PUBLISH with minimum params", function() assert.are.equal( @@ -316,6 +323,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: PUBACK[4]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("PUBACK with minimum params", function() assert.are.equal( @@ -385,6 +399,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: PUBREC[5]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("PUBREC with minimum params", function() assert.are.equal( @@ -454,6 +475,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: PUBREL[6]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("PUBREL with minimum params", function() assert.are.equal( @@ -523,6 +551,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: PUBCOMP[7]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("PUBCOMP with minimum params", function() assert.are.equal( @@ -592,6 +627,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: SUBSCRIBE[8]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("SUBSCRIBE with minimum params", function() assert.are.equal( @@ -718,6 +760,15 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +-- TODO: SUBACK[9] + +describe("MQTT v5.0 protocol: making packets: UNSUBSCRIBE[10]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("UNSUBSCRIBE with full params", function() assert.are.equal( @@ -779,6 +830,15 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +-- TODO: UNSUBACK[11] + +describe("MQTT v5.0 protocol: making packets: PINGREQ[12]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("PINGREQ", function() assert.are.equal( @@ -791,6 +851,15 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +-- TODO: PINGRESL[13] + +describe("MQTT v5.0 protocol: making packets: DISCONNECT[14]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("DISCONNECT with minimum params", function() assert.are.equal( @@ -855,6 +924,13 @@ describe("MQTT v5.0 protocol: making packets", function() })) ) end) +end) + +describe("MQTT v5.0 protocol: making packets: AUTH[15]", function() + local tools = require("mqtt.tools") + local extract_hex = require("./tools/extract_hex") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") it("AUTH with minimum params", function() assert.are.equal( diff --git a/tests/spec/protocol5-parse.lua b/tests/spec/protocol5-parse.lua index b9e9db9..60325a8 100644 --- a/tests/spec/protocol5-parse.lua +++ b/tests/spec/protocol5-parse.lua @@ -41,11 +41,226 @@ describe("MQTT v5.0 protocol: parsing packets: generic", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: CONNACK", function() +describe("MQTT v5.0 protocol: parsing packets: CONNECT[1]", function() + local mqtt = require("mqtt") + local protocol = require("mqtt.protocol") + local protocol5 = require("mqtt.protocol5") + + it("minimal properties", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = false, keep_alive = 0, client_id = "", + properties = {}, user_properties = {}, + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 0D -- variable length == 13 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + 00 -- connect flags + 0000 -- keep alive == 0 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + ]] + )) + ) + end) + + it("connect flags: clean=true", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = true, keep_alive = 0, client_id = "", + properties = {}, user_properties = {}, + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 0D -- variable length == 13 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + 02 -- connect flags: clean=true + 0000 -- keep alive == 0 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + ]] + )) + ) + end) + + it("connect flags: clean=false, will=true, no props", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = false, keep_alive = 0, client_id = "", + properties = {}, user_properties = {}, + will = { + qos = 0, retain = false, + topic = "bye", payload = "bye", + properties = {}, user_properties = {}, + } + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 18 -- variable length == 24 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + 04 -- connect flags: clean=false, will=true + 0000 -- keep alive == 0 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + 00 -- will message properties length (0 bytes) + 0003 627965 -- will message topic: 3 bytes length and string "bye" + 0003 627965 -- will message payload: 3 bytes length and bytes of string "bye" + ]] + )) + ) + end) + + it("connect flags: clean=false, will=true, will qos=2, will retain=true, and full will properties", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = false, keep_alive = 0, client_id = "", + properties = {}, user_properties = {}, + will = { + qos = 2, retain = true, + topic = "bye", payload = "bye", + properties = { + content_type = "text/plain", correlation_data = "1234", message_expiry_interval = 10, + payload_format_indicator = 1, response_topic = "resp", will_delay_interval = 30, + }, user_properties = { + {"hello", "world"}, {"hello", "again"}, + a = "b", hello = "again", + }, + } + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 64 -- variable length == 100 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + 34 -- connect flags: clean=false, will=true, will qos=2, will retain=true + 0000 -- keep alive == 0 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + + 4C -- properties length (76 bytes) + 18 0000001E -- property 0x18 == 30 -- DOC: 3.1.3.2.2 Will Delay Interval + 01 01 -- property 0x01 == 1 -- DOC: 3.1.3.2.3 Payload Format Indicator + 02 0000000A -- property 0x02 == 10 -- DOC: 3.1.3.2.4 Message Expiry Interval + 03 000A 746578742F706C61696E -- property 0x03 == "text/plain" -- DOC: 3.1.3.2.5 Content Type + 08 0004 72657370 -- property 0x08 == "resp" -- DOC: 3.1.3.2.6 Response Topic + 09 0004 31323334 -- property 0x09 == "1234" -- DOC: 3.1.3.2.7 Correlation Data + 26 0005 68656C6C6F 0005 776F726C64 -- property 0x26 (user) == ("hello", "world") -- DOC: 3.1.3.2.8 User Property + 26 0005 68656C6C6F 0005 616761696E -- property 0x26 (user) == ("hello", "again") -- DOC: 3.1.3.2.8 User Property + 26 0001 61 0001 62 -- property 0x26 (user) == ("a", "b") -- DOC: 3.1.3.2.8 User Property + + 0003 627965 -- will message topic: 3 bytes length and string "bye" + 0003 627965 -- will message payload: 3 bytes length and bytes of string "bye" + ]] + )) + ) + end) + + it("connect flags: password=true, keep_alive=30", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = false, keep_alive = 30, client_id = "", + password = "secret", + properties = {}, user_properties = {}, + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 15 -- variable length == 21 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + 40 -- connect flags: password=true + 001E -- keep alive == 30 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + 0006 736563726574 -- password length (6 bytes) and its string content - "secret" + ]] + )) + ) + end) + + it("connect flags: username=true", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = false, keep_alive = 0, client_id = "", + username = "user", + properties = {}, user_properties = {}, + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 13 -- variable length == 19 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + 80 -- connect flags: username=true + 0000 -- keep alive == 0 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + 0004 75736572 -- username length (4 bytes) and its string content - "user" + ]] + )) + ) + end) + + it("connect flags: username=true, password=true", function() + assert.are.same( + { + type=protocol.packet_type.CONNECT, + version = mqtt.v50, clean = false, keep_alive = 0, client_id = "", + username = "user", password = "secret", + properties = {}, user_properties = {}, + }, + protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 10 -- packet type == 1 (CONNECT), flags == 0 (reserved) + 1B -- variable length == 27 bytes + 0004 4D515454 -- protocol name length (4 bytes) and "MQTT" string + 05 -- protocol version: 5 (v5.0) + C0 -- connect flags: username=true, password=true + 0000 -- keep alive == 0 + 00 -- properties length (0 bytes) + 0000 -- client id length (0 bytes) and its string content (empty) + 0004 75736572 -- username length (4 bytes) and its string content - "user" + 0006 736563726574 -- password length (6 bytes) and its string content - "secret" + ]] + )) + ) + end) + +end) + +describe("MQTT v5.0 protocol: parsing packets: CONNACK[2]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") + it("CONNACK with invalid flags", function() + local packet, err = protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 21 -- packet type == 2 (CONNACK), flags == 0x1 + 03 -- variable length == 3 bytes + 00 -- 0-th bit is sp (session present) -- DOC: 3.2.2.1 Connect Acknowledge Flags + 00 -- connect reason code + 00 -- properties length + ]] + )) + assert.are.same(packet, false) + assert.are.same(err, "CONNACK: unexpected flags value: 1") + end) + it("CONNACK with minimal params and without properties", function() local packet, err = protocol5.parse_packet(make_read_func_hex( extract_hex[[ @@ -93,7 +308,7 @@ describe("MQTT v5.0 protocol: parsing packets: CONNACK", function() 01 -- 0-th bit is sp (session present) -- DOC: 3.2.2.1 Connect Acknowledge Flags 82 -- connect reason code - 72 -- properties length == 0x63 == 99 bytes + 72 -- properties length == 0x72 == 114 bytes 11 00000E10 -- property 0x11 == 3600, -- DOC: 3.2.2.3.2 Session Expiry Interval 21 1234 -- property 0x21 == 0x1234, -- DOC: 3.2.2.3.3 Receive Maximum @@ -148,7 +363,7 @@ describe("MQTT v5.0 protocol: parsing packets: CONNACK", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: PUBLISH", function() +describe("MQTT v5.0 protocol: parsing packets: PUBLISH[3]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -290,7 +505,7 @@ describe("MQTT v5.0 protocol: parsing packets: PUBLISH", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: PUBACK", function() +describe("MQTT v5.0 protocol: parsing packets: PUBACK[4]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -364,7 +579,7 @@ describe("MQTT v5.0 protocol: parsing packets: PUBACK", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: PUBREC", function() +describe("MQTT v5.0 protocol: parsing packets: PUBREC[5]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -440,7 +655,7 @@ describe("MQTT v5.0 protocol: parsing packets: PUBREC", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: PUBREL", function() +describe("MQTT v5.0 protocol: parsing packets: PUBREL[6]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -452,7 +667,7 @@ describe("MQTT v5.0 protocol: parsing packets: PUBREL", function() 02 -- variable length == 2 bytes 0003 -- packet_id to acknowledge - -- no reason code and properties + -- no reason code and no properties ]] )) assert.is_nil(err) @@ -514,7 +729,7 @@ describe("MQTT v5.0 protocol: parsing packets: PUBREL", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: PUBCOMP", function() +describe("MQTT v5.0 protocol: parsing packets: PUBCOMP[7]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -588,7 +803,141 @@ describe("MQTT v5.0 protocol: parsing packets: PUBCOMP", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: SUBACK", function() + +describe("MQTT v5.0 protocol: parsing packets: SUBSCRIBE[8]", function() + local protocol = require("mqtt.protocol") + local pt = assert(protocol.packet_type) + local protocol5 = require("mqtt.protocol5") + + it("with minimal params, without properties", function() + local packet, err = protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 82 -- packet type == 8 (SUBSCRIBE), flags == 0x2 + 0A -- variable length == 10 bytes + + 0005 -- packet_id + 00 -- properties length == 0 + + 0004 74657374 -- topic name == "test" + 00 -- Subscription Options == 0 + ]] + )) + assert.is_nil(err) + assert.are.same( + { + type=pt.SUBSCRIBE, packet_id=5, properties={}, user_properties={}, + subscriptions = { + { + topic = "test", + no_local = false, + qos = 0, + retain_as_published = false, + retain_handling = 0, + }, + }, + }, + packet + ) + end) + + it("with properties and several subscriptions", function() + local packet, err = protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 82 -- packet type == 8 (SUBSCRIBE), flags == 0x2 + 3C -- variable length == 60 bytes + + 0005 -- packet_id + 11 -- properties length == 17 + + 0B 01 -- property 0x0B == 1 -- DOC: 3.3.2.3.8 Subscription Identifier + 26 0005 68656C6C6F 0005 776F726C64 -- property 0x26 (user) == ("hello", "world") -- DOC: 3.2.2.3.10 User Property + + 0005 7465737431 -- topic name == "test1" + 00 -- Subscription Options == 0 + 0005 7465737432 -- topic name == "test2" + 01 -- Subscription Options == 1 (qos=1) + 0005 7465737433 -- topic name == "test3" + 06 -- Subscription Options == 6 (qos=2, no_local=true) + 0005 7465737434 -- topic name == "test4" + 0A -- Subscription Options == 12 (qos=2, retain_as_published=true) + 0005 7465737435 -- topic name == "test5" + 1E -- Subscription Options == 30 (qos=2, no_local=true, retain_as_published=true, retain_handling=1) + ]] + )) + assert.is_nil(err) + assert.are.same( + { + type=pt.SUBSCRIBE, packet_id=5, + properties={ + subscription_identifiers = 1, + }, + user_properties={ + hello = "world", + }, + subscriptions = { + { + topic = "test1", + no_local = false, + qos = 0, + retain_as_published = false, + retain_handling = 0, + }, + { + topic = "test2", + no_local = false, + qos = 1, + retain_as_published = false, + retain_handling = 0, + }, + { + topic = "test3", + no_local = true, + qos = 2, + retain_as_published = false, + retain_handling = 0, + }, + { + topic = "test4", + no_local = false, + qos = 2, + retain_as_published = true, + retain_handling = 0, + }, + { + topic = "test5", + no_local = true, + qos = 2, + retain_as_published = true, + retain_handling = 1, + }, + }, + }, + packet + ) + end) + + it("with invalid properties", function() + local packet, err = protocol5.parse_packet(make_read_func_hex( + extract_hex[[ + 82 -- packet type == 8 (SUBSCRIBE), flags == 0x2 + 0E -- variable length == 14 bytes + + 0005 -- packet_id + 04 -- properties length == 0 + + 0B 01 -- property 0x0B == 1 -- DOC: 3.3.2.3.8 Subscription Identifier + 0B 02 -- property 0x0B == 2 -- DOC: 3.3.2.3.8 Subscription Identifier (It is a Protocol Error to include the Subscription Identifier more than once) + + 0004 74657374 -- topic name == "test" + 00 -- Subscription Options == 0 + ]] + )) + assert.are.same('SUBSCRIBE: failed to parse packet properties: it is a Protocol Error to include the subscription_identifiers (11) property more than once', err) + assert.are.same(false, packet) + end) +end) + +describe("MQTT v5.0 protocol: parsing packets: SUBACK[9]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -672,7 +1021,9 @@ describe("MQTT v5.0 protocol: parsing packets: SUBACK", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: UNSUBACK", function() +-- TODO: UNSUBSCRIBE[10] + +describe("MQTT v5.0 protocol: parsing packets: UNSUBACK[11]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -756,7 +1107,9 @@ describe("MQTT v5.0 protocol: parsing packets: UNSUBACK", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: PINGRESP", function() +-- TODO: PINGREQ[12] + +describe("MQTT v5.0 protocol: parsing packets: PINGRESP[13]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -778,7 +1131,7 @@ describe("MQTT v5.0 protocol: parsing packets: PINGRESP", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: DISCONNECT", function() +describe("MQTT v5.0 protocol: parsing packets: DISCONNECT[14]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5") @@ -871,7 +1224,7 @@ describe("MQTT v5.0 protocol: parsing packets: DISCONNECT", function() end) end) -describe("MQTT v5.0 protocol: parsing packets: AUTH", function() +describe("MQTT v5.0 protocol: parsing packets: AUTH[15]", function() local protocol = require("mqtt.protocol") local pt = assert(protocol.packet_type) local protocol5 = require("mqtt.protocol5")