1040 lines
40 KiB
Lua
1040 lines
40 KiB
Lua
--[[
|
||
|
||
Here is a MQTT v5.0 protocol implementation
|
||
|
||
MQTT v5.0 documentation (DOC):
|
||
http://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
|
||
|
||
]]
|
||
|
||
-- module table
|
||
local protocol5 = {}
|
||
|
||
-- load required stuff
|
||
local type = type
|
||
local error = error
|
||
local assert = assert
|
||
local require = require
|
||
local tostring = tostring
|
||
local setmetatable = setmetatable
|
||
|
||
local table = require("table")
|
||
local unpack = table.unpack or unpack
|
||
local tbl_sort = table.sort
|
||
|
||
local string = require("string")
|
||
local str_char = string.char
|
||
local fmt = string.format
|
||
|
||
local bit = require("mqtt.bitwrap")
|
||
local bor = bit.bor
|
||
local band = bit.band
|
||
local lshift = bit.lshift
|
||
local rshift = bit.rshift
|
||
|
||
local protocol = require("mqtt.protocol")
|
||
local make_uint8 = protocol.make_uint8
|
||
local make_uint16 = protocol.make_uint16
|
||
local make_uint32 = protocol.make_uint32
|
||
local make_string = protocol.make_string
|
||
local make_var_length = protocol.make_var_length
|
||
local parse_var_length = protocol.parse_var_length
|
||
local make_uint8_0_or_1 = protocol.make_uint8_0_or_1
|
||
local make_uint16_nonzero = protocol.make_uint16_nonzero
|
||
local make_var_length_nonzero = protocol.make_var_length_nonzero
|
||
local parse_string = protocol.parse_string
|
||
local parse_uint8 = protocol.parse_uint8
|
||
local parse_uint8_0_or_1 = protocol.parse_uint8_0_or_1
|
||
local parse_uint16 = protocol.parse_uint16
|
||
local parse_uint16_nonzero = protocol.parse_uint16_nonzero
|
||
local parse_uint32 = protocol.parse_uint32
|
||
local parse_var_length_nonzero = protocol.parse_var_length_nonzero
|
||
local make_header = protocol.make_header
|
||
local check_qos = protocol.check_qos
|
||
local check_packet_id = protocol.check_packet_id
|
||
local combine = protocol.combine
|
||
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
|
||
|
||
-- Returns true if given value is a valid Retain Handling option, DOC: 3.8.3.1 Subscription Options
|
||
local function check_retain_handling(val)
|
||
return (val == 0) or (val == 1) or (val == 2)
|
||
end
|
||
|
||
-- Create Connect Flags data, DOC: 3.1.2.3 Connect Flags
|
||
local function make_connect_flags(args)
|
||
local byte = 0 -- bit 0 should be zero
|
||
-- DOC: 3.1.2.4 Clean Start
|
||
if args.clean ~= nil then
|
||
assert(type(args.clean) == "boolean", "expecting .clean to be a boolean")
|
||
if args.clean then
|
||
byte = bor(byte, lshift(1, 1))
|
||
end
|
||
end
|
||
-- DOC: 3.1.2.5 Will Flag
|
||
if args.will ~= nil then
|
||
-- check required args are presented
|
||
assert(type(args.will) == "table", "expecting .will to be a table")
|
||
assert(type(args.will.payload) == "string", "expecting .will.payload to be a string")
|
||
assert(type(args.will.topic) == "string", "expecting .will.topic to be a string")
|
||
assert(type(args.will.qos) == "number", "expecting .will.qos to be a number")
|
||
assert(check_qos(args.will.qos), "expecting .will.qos to be a valid QoS value")
|
||
assert(type(args.will.retain) == "boolean", "expecting .will.retain to be a boolean")
|
||
if args.will.properties ~= nil then
|
||
assert(type(args.will.properties) == "table", "expecting .will.properties to be a table")
|
||
end
|
||
if args.will.user_properties ~= nil then
|
||
assert(type(args.will.user_properties) == "table", "expecting .will.user_properties to be a table")
|
||
end
|
||
-- will flag should be set to 1
|
||
byte = bor(byte, lshift(1, 2))
|
||
-- DOC: 3.1.2.6 Will QoS
|
||
byte = bor(byte, lshift(args.will.qos, 3))
|
||
-- DOC: 3.1.2.7 Will Retain
|
||
if args.will.retain then
|
||
byte = bor(byte, lshift(1, 5))
|
||
end
|
||
end
|
||
-- DOC: 3.1.2.8 User Name Flag
|
||
if args.username ~= nil then
|
||
assert(type(args.username) == "string", "expecting .username to be a string")
|
||
byte = bor(byte, lshift(1, 7))
|
||
end
|
||
-- DOC: 3.1.2.9 Password Flag
|
||
if args.password ~= nil then
|
||
assert(type(args.password) == "string", "expecting .password to be a string")
|
||
assert(args.username, "the .username is required to set .password")
|
||
byte = bor(byte, lshift(1, 6))
|
||
end
|
||
return make_uint8(byte)
|
||
end
|
||
|
||
-- Known property names and its identifiers, DOC: 2.2.2.2 Property
|
||
local property_pairs = {
|
||
{ 0x01, "payload_format_indicator",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x02, "message_expiry_interval",
|
||
make = make_uint32,
|
||
parse = parse_uint32, },
|
||
{ 0x03, "content_type",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x08, "response_topic",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x09, "correlation_data",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x0B, "subscription_identifiers",
|
||
make = function(value) return str_char(make_var_length_nonzero(value)) end,
|
||
parse = parse_var_length_nonzero,
|
||
multiple = true, },
|
||
{ 0x11, "session_expiry_interval",
|
||
make = make_uint32,
|
||
parse = parse_uint32, },
|
||
{ 0x12, "assigned_client_identifier",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x13, "server_keep_alive",
|
||
make = make_uint16,
|
||
parse = parse_uint16, },
|
||
{ 0x15, "authentication_method",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x16, "authentication_data",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x17, "request_problem_information",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x18, "will_delay_interval",
|
||
make = make_uint32,
|
||
parse = parse_uint32, },
|
||
{ 0x19, "request_response_information",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x1A, "response_information",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x1C, "server_reference",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x1F, "reason_string",
|
||
make = make_string,
|
||
parse = parse_string, },
|
||
{ 0x21, "receive_maximum",
|
||
make = make_uint16,
|
||
parse = parse_uint16, },
|
||
{ 0x22, "topic_alias_maximum",
|
||
make = make_uint16,
|
||
parse = parse_uint16, },
|
||
{ 0x23, "topic_alias",
|
||
make = make_uint16_nonzero,
|
||
parse = parse_uint16_nonzero, },
|
||
{ 0x24, "maximum_qos",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x25, "retain_available",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x26, "user_property", -- NOTE: not implemented intentionally
|
||
make = function(value_) error("not implemented") end,
|
||
parse = function(read_func_) error("not implemented") end, },
|
||
{ 0x27, "maximum_packet_size",
|
||
make = make_uint32,
|
||
parse = parse_uint32, },
|
||
{ 0x28, "wildcard_subscription_available",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x29, "subscription_identifiers_available",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
{ 0x2A, "shared_subscription_available",
|
||
make = make_uint8_0_or_1,
|
||
parse = parse_uint8_0_or_1, },
|
||
}
|
||
|
||
-- properties table with keys in two directions: from name to identifier and back
|
||
local properties = {}
|
||
-- table with property value make functions
|
||
local property_make = {}
|
||
-- table with property value parse function
|
||
local property_parse = {}
|
||
-- table with property multiple flag
|
||
local property_multiple = {}
|
||
-- fill the properties and property_make tables
|
||
for _, prop in ipairs(property_pairs) do
|
||
properties[prop[2]] = prop[1] -- name ==> identifier
|
||
properties[prop[1]] = prop[2] -- identifier ==> name
|
||
property_make[prop[1]] = prop.make -- identifier ==> make function
|
||
property_parse[prop[1]] = prop.parse -- identifier ==> make function
|
||
property_multiple[prop[1]] = prop.multiple -- identifier ==> multiple flag
|
||
end
|
||
|
||
-- Allowed properties per packet type
|
||
local allowed_properties = {
|
||
[packet_type.CONNECT] = {
|
||
[0x11] = true, -- DOC: 3.1.2.11.2 Session Expiry Interval
|
||
[0x21] = true, -- DOC: 3.1.2.11.3 Receive Maximum
|
||
[0x27] = true, -- DOC: 3.1.2.11.4 Maximum Packet Size
|
||
[0x22] = true, -- DOC: 3.1.2.11.5 Topic Alias Maximum
|
||
[0x19] = true, -- DOC: 3.1.2.11.6 Request Response Information
|
||
[0x17] = true, -- DOC: 3.1.2.11.7 Request Problem Information
|
||
[0x26] = true, -- DOC: 3.1.2.11.8 User Property
|
||
[0x15] = true, -- DOC: 3.1.2.11.9 Authentication Method
|
||
[0x16] = true, -- DOC: 3.1.2.11.10 Authentication Data
|
||
},
|
||
[packet_type.CONNACK] = {
|
||
[0x11] = true, -- DOC: 3.2.2.3.2 Session Expiry Interval
|
||
[0x21] = true, -- DOC: 3.2.2.3.3 Receive Maximum
|
||
[0x24] = true, -- DOC: 3.2.2.3.4 Maximum QoS
|
||
[0x25] = true, -- DOC: 3.2.2.3.5 Retain Available
|
||
[0x27] = true, -- DOC: 3.2.2.3.6 Maximum Packet Size
|
||
[0x12] = true, -- DOC: 3.2.2.3.7 Assigned Client Identifier
|
||
[0x22] = true, -- DOC: 3.2.2.3.8 Topic Alias Maximum
|
||
[0x1F] = true, -- DOC: 3.2.2.3.9 Reason String
|
||
[0x26] = true, -- DOC: 3.2.2.3.10 User Property
|
||
[0x28] = true, -- DOC: 3.2.2.3.11 Wildcard Subscription Available
|
||
[0x29] = true, -- DOC: 3.2.2.3.12 Subscription Identifiers Available
|
||
[0x2A] = true, -- DOC: 3.2.2.3.13 Shared Subscription Available
|
||
[0x13] = true, -- DOC: 3.2.2.3.14 Server Keep Alive
|
||
[0x1A] = true, -- DOC: 3.2.2.3.15 Response Information
|
||
[0x1C] = true, -- DOC: 3.2.2.3.16 Server Reference
|
||
[0x15] = true, -- DOC: 3.2.2.3.17 Authentication Method
|
||
[0x16] = true, -- DOC: 3.2.2.3.18 Authentication Data
|
||
},
|
||
[packet_type.PUBLISH] = {
|
||
[0x01] = true, -- DOC: 3.3.2.3.2 Payload Format Indicator
|
||
[0x02] = true, -- DOC: 3.3.2.3.3 Message Expiry Interval
|
||
[0x23] = true, -- DOC: 3.3.2.3.4 Topic Alias
|
||
[0x08] = true, -- DOC: 3.3.2.3.5 Response Topic
|
||
[0x09] = true, -- DOC: 3.3.2.3.6 Correlation Data
|
||
[0x26] = true, -- DOC: 3.3.2.3.7 User Property
|
||
[0x0B] = true, -- DOC: 3.3.2.3.8 Subscription Identifier
|
||
[0x03] = true, -- DOC: 3.3.2.3.9 Content Type
|
||
},
|
||
will = {
|
||
[0x18] = true, -- DOC: 3.1.3.2.2 Will Delay Interval
|
||
[0x01] = true, -- DOC: 3.1.3.2.3 Payload Format Indicator
|
||
[0x02] = true, -- DOC: 3.1.3.2.4 Message Expiry Interval
|
||
[0x03] = true, -- DOC: 3.1.3.2.5 Content Type
|
||
[0x08] = true, -- DOC: 3.1.3.2.6 Response Topic
|
||
[0x09] = true, -- DOC: 3.1.3.2.7 Correlation Data
|
||
[0x26] = true, -- DOC: 3.1.3.2.8 User Property
|
||
},
|
||
[packet_type.PUBACK] = {
|
||
[0x1F] = true, -- DOC: 3.4.2.2.2 Reason String
|
||
[0x26] = true, -- DOC: 3.4.2.2.3 User Property
|
||
},
|
||
[packet_type.PUBREC] = {
|
||
[0x1F] = true, -- DOC: 3.5.2.2.2 Reason String
|
||
[0x26] = true, -- DOC: 3.5.2.2.3 User Property
|
||
},
|
||
[packet_type.PUBREL] = {
|
||
[0x1F] = true, -- DOC: 3.6.2.2.2 Reason String
|
||
[0x26] = true, -- DOC: 3.6.2.2.3 User Property
|
||
},
|
||
[packet_type.PUBCOMP] = {
|
||
[0x1F] = true, -- DOC: 3.7.2.2.2 Reason String
|
||
[0x26] = true, -- DOC: 3.7.2.2.3 User Property
|
||
},
|
||
[packet_type.SUBSCRIBE] = {
|
||
[0x0B] = true, -- DOC: 3.8.2.1.2 Subscription Identifier
|
||
[0x26] = true, -- DOC: 3.8.2.1.3 User Property
|
||
},
|
||
[packet_type.SUBACK] = {
|
||
[0x1F] = true, -- DOC: 3.9.2.1.2 Reason String
|
||
[0x26] = true, -- DOC: 3.9.2.1.3 User Property
|
||
},
|
||
[packet_type.UNSUBSCRIBE] = {
|
||
[0x26] = true, -- DOC: 3.10.2.1.2 User Property
|
||
},
|
||
[packet_type.UNSUBACK] = {
|
||
[0x1F] = true, -- DOC: 3.11.2.1.2 Reason String
|
||
[0x26] = true, -- DOC: 3.11.2.1.3 User Property
|
||
},
|
||
-- NOTE: PINGREQ (3.12), PINGRESP (3.13) has no properties
|
||
[packet_type.DISCONNECT] = {
|
||
[0x11] = true, -- DOC: 3.14.2.2.2 Session Expiry Interval
|
||
[0x1F] = true, -- DOC: 3.14.2.2.3 Reason String
|
||
[0x26] = true, -- DOC: 3.14.2.2.4 User Property
|
||
[0x1C] = true, -- DOC: 3.14.2.2.5 Server Reference
|
||
},
|
||
[packet_type.AUTH] = {
|
||
[0x15] = true, -- DOC: 3.15.2.2.2 Authentication Method
|
||
[0x16] = true, -- DOC: 3.15.2.2.3 Authentication Data
|
||
[0x1F] = true, -- DOC: 3.15.2.2.4 Reason String
|
||
[0x26] = true, -- DOC: 3.15.2.2.5 User Property
|
||
},
|
||
}
|
||
|
||
-- Create properties field for various control packets, DOC: 2.2.2 Properties
|
||
local function make_properties(ptype, args)
|
||
local allowed = assert(allowed_properties[ptype], "invalid packet type to detect allowed properties")
|
||
local props = ""
|
||
local uprop_id = properties.user_property
|
||
-- writing known properties
|
||
if args.properties ~= nil then
|
||
assert(type(args.properties) == "table", "expecting .properties to be a table")
|
||
-- validate all properties and append them to order list
|
||
local order = {}
|
||
for name, value in pairs(args.properties) do
|
||
assert(type(name) == "string", "expecting property name to be a string: "..tostring(name))
|
||
-- detect property identifier and check it's allowed for that packet type
|
||
local prop_id = assert(properties[name], "unknown property: "..tostring(name))
|
||
assert(prop_id ~= uprop_id, "user properties should be passed in .user_properties table")
|
||
assert(allowed[prop_id], "property "..name.." is not allowed for packet type "..ptype)
|
||
order[#order + 1] = { prop_id, name, value }
|
||
end
|
||
-- sort props in the identifier ascending order
|
||
tbl_sort(order, function(a, b) return a[1] < b[1] end)
|
||
for _, item in ipairs(order) do
|
||
local prop_id, name, value = unpack(item)
|
||
if property_multiple[prop_id] then
|
||
assert(type(value) == "table", "expecting list-table for property with multiple value")
|
||
assert(#value == 1, "only one value for multiple-property supported")
|
||
value = value[1]
|
||
end
|
||
-- make property data
|
||
local ok, val = pcall(property_make[prop_id], value)
|
||
if not ok then
|
||
error("invalid property value: "..name.." = "..tostring(value)..": "..tostring(val))
|
||
end
|
||
local prop = combine(
|
||
str_char(make_var_length(prop_id)),
|
||
val
|
||
)
|
||
-- and append it to props
|
||
if type(props) == "string" then
|
||
props = combine(prop)
|
||
else
|
||
props:append(prop)
|
||
end
|
||
end
|
||
end
|
||
-- writing userproperties
|
||
if args.user_properties ~= nil then
|
||
assert(type(args.user_properties) == "table", "expecting .user_properties to be a table")
|
||
assert(allowed[uprop_id], "user_property is not allowed for packet type "..ptype)
|
||
local order = {}
|
||
for name, val in pairs(args.user_properties) do
|
||
local ntype = type(name)
|
||
if ntype == "string" then
|
||
if type(val) ~= "string" then
|
||
error(fmt("user property '%s' value should be a string", name))
|
||
end
|
||
order[#order + 1] = {name, val, 0}
|
||
elseif ntype == "number" then
|
||
if type(val) ~= "table" or type(val[1]) ~= "string" or type(val[2]) ~= "string" then
|
||
error(fmt("user property at index %d should be a table with two strings", name))
|
||
end
|
||
order[#order + 1] = {val[1], val[2], name}
|
||
else
|
||
error(fmt("unknown user property name type passed: %s", ntype))
|
||
end
|
||
end
|
||
tbl_sort(order, function(a, b) if a[1] == b[1] then return a[3] < b[3] else return a[1] < b[1] end end)
|
||
for _, pair in ipairs(order) do
|
||
local name = pair[1]
|
||
local value = pair[2]
|
||
-- make user property data
|
||
local prop = combine(
|
||
str_char(make_var_length(uprop_id)),
|
||
make_string(name),
|
||
make_string(value)
|
||
)
|
||
-- and append it to props
|
||
if type(props) == "string" then
|
||
props = combine(prop)
|
||
else
|
||
props:append(prop)
|
||
end
|
||
end
|
||
end
|
||
-- and combine properties with its length field
|
||
return combine(
|
||
str_char(make_var_length(props:len())), -- DOC: 2.2.2.1 Property Length
|
||
props -- DOC: 2.2.2.2 Property
|
||
)
|
||
end
|
||
|
||
-- Create CONNECT packet, DOC: 3.1 CONNECT – Connection Request
|
||
local function make_packet_connect(args)
|
||
-- check args
|
||
assert(type(args.id) == "string", "expecting .id to be a string with MQTT client id")
|
||
-- DOC: 3.1.2.10 Keep Alive
|
||
local keep_alive_ival = 0
|
||
if args.keep_alive then
|
||
assert(type(args.keep_alive) == "number")
|
||
keep_alive_ival = args.keep_alive
|
||
end
|
||
-- DOC: 3.1.2.11 CONNECT Properties
|
||
local props = make_properties(packet_type.CONNECT, args)
|
||
-- DOC: 3.1.2 CONNECT Variable Header
|
||
local variable_header = combine(
|
||
make_string("MQTT"), -- DOC: 3.1.2.1 Protocol Name
|
||
make_uint8(5), -- DOC: 3.1.2.2 Protocol Version (5 is for MQTT v5.0)
|
||
make_connect_flags(args), -- DOC: 3.1.2.3 Connect Flags
|
||
make_uint16(keep_alive_ival), -- DOC: 3.1.2.10 Keep Alive
|
||
props -- DOC: 3.1.2.11 CONNECT Properties
|
||
)
|
||
-- DOC: 3.1.3 CONNECT Payload
|
||
-- DOC: 3.1.3.1 Client Identifier (ClientID)
|
||
local payload = combine(
|
||
make_string(args.id)
|
||
)
|
||
if args.will then
|
||
-- DOC: 3.1.3.2 Will Properties
|
||
payload:append(make_properties("will", args.will))
|
||
-- DOC: 3.1.3.3 Will Topic
|
||
assert(type(args.will.topic) == "string", "expecting will.topic to be a string")
|
||
payload:append(make_string(args.will.topic))
|
||
-- DOC: 3.1.3.4 Will Payload
|
||
assert(args.will.payload == nil or type(args.will.payload) == "string", "expecting will.payload to be a string or nil")
|
||
payload:append(make_string(args.will.payload))
|
||
end
|
||
if args.username then
|
||
-- DOC: 3.1.3.5 User Name
|
||
payload:append(make_string(args.username))
|
||
if args.password then
|
||
-- DOC: 3.1.3.6 Password
|
||
payload:append(make_string(args.password))
|
||
end
|
||
end
|
||
-- DOC: 3.1.1 Fixed header
|
||
local header = make_header(packet_type.CONNECT, 0, variable_header:len() + payload:len())
|
||
return combine(header, variable_header, payload)
|
||
end
|
||
|
||
-- Create PUBLISH packet, DOC: 3.3 PUBLISH – Publish message
|
||
local function make_packet_publish(args)
|
||
-- check args
|
||
assert(type(args.topic) == "string", "expecting .topic to be a string")
|
||
if args.payload ~= nil then
|
||
assert(type(args.payload) == "string", "expecting .payload to be a string")
|
||
end
|
||
if args.qos ~= nil then
|
||
assert(type(args.qos) == "number", "expecting .qos to be a number")
|
||
assert(check_qos(args.qos), "expecting .qos to be a valid QoS value")
|
||
end
|
||
if args.retain ~= nil then
|
||
assert(type(args.retain) == "boolean", "expecting .retain to be a boolean")
|
||
end
|
||
if args.dup ~= nil then
|
||
assert(type(args.dup) == "boolean", "expecting .dup to be a boolean")
|
||
end
|
||
|
||
-- DOC: 3.3.1 PUBLISH Fixed Header
|
||
local flags = 0
|
||
-- 3.3.1.3 RETAIN
|
||
if args.retain then
|
||
flags = bor(flags, 0x1)
|
||
end
|
||
-- DOC: 3.3.1.2 QoS
|
||
flags = bor(flags, lshift(args.qos or 0, 1))
|
||
-- DOC: 3.3.1.1 DUP
|
||
if args.dup then
|
||
flags = bor(flags, lshift(1, 3))
|
||
end
|
||
-- DOC: 3.3.2 PUBLISH Variable Header
|
||
local variable_header = combine(
|
||
make_string(args.topic)
|
||
)
|
||
-- DOC: 3.3.2.2 Packet Identifier
|
||
if args.qos and args.qos > 0 then
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
variable_header:append(make_uint16(args.packet_id))
|
||
end
|
||
-- DOC: 3.3.2.3 PUBLISH Properties
|
||
variable_header:append(make_properties(packet_type.PUBLISH, args))
|
||
-- DOC: 3.3.3 PUBLISH Payload
|
||
local payload
|
||
if args.payload then
|
||
payload = args.payload
|
||
else
|
||
payload = ""
|
||
end
|
||
-- DOC: 3.3.1 Fixed header
|
||
local header = make_header(packet_type.PUBLISH, flags, variable_header:len() + payload:len())
|
||
return combine(header, variable_header, payload)
|
||
end
|
||
|
||
-- Create PUBACK packet, DOC: 3.4 PUBACK – Publish acknowledgement
|
||
local function make_packet_puback(args)
|
||
-- check args
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
assert(type(args.rc) == "number", "expecting .rc to be a number")
|
||
-- DOC: 3.4.2 PUBACK Variable Header
|
||
local variable_header = combine(make_uint16(args.packet_id))
|
||
local props = make_properties(packet_type.PUBACK, args) -- DOC: 3.4.2.2 PUBACK Properties
|
||
-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBACK has a Remaining Length of 2.
|
||
if props:len() > 1 or args.rc ~= 0 then
|
||
variable_header:append(make_uint8(args.rc)) -- DOC: 3.4.2.1 PUBACK Reason Code
|
||
variable_header:append(props) -- DOC: 3.4.2.2 PUBACK Properties
|
||
end
|
||
-- DOC: 3.4.1 PUBACK Fixed Header
|
||
local header = make_header(packet_type.PUBACK, 0, variable_header:len())
|
||
return combine(header, variable_header)
|
||
end
|
||
|
||
-- Create PUBREC packet, DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1)
|
||
local function make_packet_pubrec(args)
|
||
-- check args
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
assert(type(args.rc) == "number", "expecting .rc to be a number")
|
||
-- DOC: 3.5.2 PUBREC Variable Header
|
||
local variable_header = combine(make_uint16(args.packet_id))
|
||
local props = make_properties(packet_type.PUBREC, args) -- DOC: 3.5.2.2 PUBREC Properties
|
||
-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBREC has a Remaining Length of 2.
|
||
if props:len() > 1 or args.rc ~= 0 then
|
||
variable_header:append(make_uint8(args.rc)) -- DOC: 3.5.2.1 PUBREC Reason Code
|
||
variable_header:append(props) -- DOC: 3.5.2.2 PUBREC Properties
|
||
end
|
||
-- DOC: 3.5.1 PUBREC Fixed Header
|
||
local header = make_header(packet_type.PUBREC, 0, variable_header:len())
|
||
return combine(header, variable_header)
|
||
end
|
||
|
||
-- Create PUBREL packet, DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2)
|
||
local function make_packet_pubrel(args)
|
||
-- check args
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
assert(type(args.rc) == "number", "expecting .rc to be a number")
|
||
-- DOC: 3.6.2 PUBREL Variable Header
|
||
local variable_header = combine(make_uint16(args.packet_id))
|
||
local props = make_properties(packet_type.PUBREL, args) -- DOC: 3.6.2.2 PUBREL Properties
|
||
-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBREL has a Remaining Length of 2.
|
||
if props:len() > 1 or args.rc ~= 0 then
|
||
variable_header:append(make_uint8(args.rc)) -- DOC: 3.6.2.1 PUBREL Reason Code
|
||
variable_header:append(props) -- DOC: 3.6.2.2 PUBREL Properties
|
||
end
|
||
-- DOC: 3.6.1 PUBREL Fixed Header
|
||
local header = make_header(packet_type.PUBREL, 2, variable_header:len()) -- flags: fixed 0010 bits, DOC: Figure 3‑14 – PUBREL packet Fixed Header
|
||
return combine(header, variable_header)
|
||
end
|
||
|
||
-- Create PUBCOMP packet, DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3)
|
||
local function make_packet_pubcomp(args)
|
||
-- check args
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
assert(type(args.rc) == "number", "expecting .rc to be a number")
|
||
-- DOC: 3.7.2 PUBCOMP Variable Header
|
||
local variable_header = combine(make_uint16(args.packet_id))
|
||
local props = make_properties(packet_type.PUBCOMP, args) -- DOC: 3.7.2.2 PUBCOMP Properties
|
||
-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBCOMP has a Remaining Length of 2.
|
||
if props:len() > 1 or args.rc ~= 0 then
|
||
variable_header:append(make_uint8(args.rc)) -- DOC: 3.7.2.1 PUBCOMP Reason Code
|
||
variable_header:append(props) -- DOC: 3.7.2.2 PUBCOMP Properties
|
||
end
|
||
-- DOC: 3.7.1 PUBCOMP Fixed Header
|
||
local header = make_header(packet_type.PUBCOMP, 0, variable_header:len())
|
||
return combine(header, variable_header)
|
||
end
|
||
|
||
-- Create SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe request
|
||
local function make_packet_subscribe(args)
|
||
-- check args
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
|
||
assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
|
||
-- DOC: 3.8.2 SUBSCRIBE Variable Header
|
||
local variable_header = combine(
|
||
make_uint16(args.packet_id),
|
||
make_properties(packet_type.SUBSCRIBE, args) -- DOC: 3.8.2.1 SUBSCRIBE Properties
|
||
)
|
||
-- DOC: 3.8.3 SUBSCRIBE Payload
|
||
local payload = combine()
|
||
for i, subscription in ipairs(args.subscriptions) do
|
||
assert(type(subscription) == "table", "expecting .subscriptions["..i.."] to be a table")
|
||
assert(type(subscription.topic) == "string", "expecting .subscriptions["..i.."].topic to be a string")
|
||
if subscription.qos ~= nil then -- TODO: maybe remove that check and make .qos mandatory?
|
||
assert(type(subscription.qos) == "number", "expecting .subscriptions["..i.."].qos to be a number")
|
||
assert(check_qos(subscription.qos), "expecting .subscriptions["..i.."].qos to be a valid QoS value")
|
||
end
|
||
if subscription.retain_as_published ~= nil then
|
||
assert(type(subscription.retain_as_published) == "boolean", "expecting .subscriptions["..i.."].retain_as_published to be a boolean")
|
||
end
|
||
if subscription.retain_handling ~= nil then
|
||
assert(type(subscription.retain_handling) == "number", "expecting .subscriptions["..i.."].retain_handling to be a number")
|
||
assert(check_retain_handling(subscription.retain_handling), "expecting .subscriptions["..i.."].retain_handling to be a valid Retain Handling option")
|
||
end
|
||
-- DOC: 3.8.3.1 Subscription Options
|
||
local so = subscription.qos or 0
|
||
if subscription.no_local then
|
||
so = bor(so, 4) -- set Bit 2
|
||
end
|
||
if subscription.retain_as_published then
|
||
so = bor(so, 8) -- set Bit 3
|
||
end
|
||
if subscription.retain_handling then
|
||
so = bor(so, lshift(subscription.retain_handling, 4)) -- set Bit 4 and 5
|
||
end
|
||
payload:append(make_string(subscription.topic))
|
||
payload:append(make_uint8(so))
|
||
end
|
||
-- DOC: 3.8.1 SUBSCRIBE Fixed Header
|
||
local header = make_header(packet_type.SUBSCRIBE, 2, variable_header:len() + payload:len()) -- flags: fixed 0010 bits, DOC: Figure 3‑18 SUBSCRIBE packet Fixed Header
|
||
return combine(header, variable_header, payload)
|
||
end
|
||
|
||
-- Create UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE – Unsubscribe request
|
||
local function make_packet_unsubscribe(args)
|
||
-- check args
|
||
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
|
||
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
|
||
assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
|
||
assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
|
||
-- DOC: 3.10.2 UNSUBSCRIBE Variable Header
|
||
local variable_header = combine(
|
||
make_uint16(args.packet_id),
|
||
make_properties(packet_type.UNSUBSCRIBE, args) -- DOC: 3.10.2.1 UNSUBSCRIBE Properties
|
||
)
|
||
-- DOC: 3.10.3 UNSUBSCRIBE Payload
|
||
local payload = combine()
|
||
for i, subscription in ipairs(args.subscriptions) do
|
||
assert(type(subscription) == "string", "expecting .subscriptions["..i.."] to be a string")
|
||
payload:append(make_string(subscription))
|
||
end
|
||
-- DOC: 3.10.1 UNSUBSCRIBE Fixed Header
|
||
local header = make_header(packet_type.UNSUBSCRIBE, 2, variable_header:len() + payload:len()) -- flags: fixed 0010 bits, DOC: Figure 3.28 – UNSUBSCRIBE packet Fixed Header
|
||
return combine(header, variable_header, payload)
|
||
end
|
||
|
||
-- Create DISCONNECT packet, DOC: 3.14 DISCONNECT – Disconnect notification
|
||
local function make_packet_disconnect(args)
|
||
-- check args
|
||
assert(type(args.rc) == "number", "expecting .rc to be a number")
|
||
-- DOC: 3.14.2 DISCONNECT Variable Header
|
||
local variable_header = combine()
|
||
local props = make_properties(packet_type.DISCONNECT, args) -- DOC: 3.14.2.2 DISCONNECT Properties
|
||
-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Normal disconnecton) and there are no Properties. In this case the DISCONNECT has a Remaining Length of 0.
|
||
if props:len() > 1 or args.rc ~= 0 then
|
||
variable_header:append(make_uint8(args.rc)) -- DOC: 3.14.2.1 Disconnect Reason Code
|
||
variable_header:append(props) -- DOC: 3.14.2.2 DISCONNECT Properties
|
||
end
|
||
-- DOC: 3.14.1 DISCONNECT Fixed Header
|
||
local header = make_header(packet_type.DISCONNECT, 0, variable_header:len()) -- flags: 0
|
||
return combine(header, variable_header)
|
||
end
|
||
|
||
-- Create AUTH packet, DOC: 3.15 AUTH – Authentication exchange
|
||
local function make_packet_auth(args)
|
||
-- check args
|
||
assert(type(args.rc) == "number", "expecting .rc to be a number")
|
||
-- DOC: 3.15.2 AUTH Variable Header
|
||
local variable_header = combine()
|
||
-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the AUTH has a Remaining Length of 0.
|
||
local props = make_properties(packet_type.AUTH, args) -- DOC: 3.15.2.2 AUTH Properties
|
||
if props:len() > 1 or args.rc ~= 0 then
|
||
variable_header:append(make_uint8(args.rc)) -- DOC: 3.15.2.1 Authenticate Reason Code
|
||
variable_header:append(props) -- DOC: 3.15.2.2 AUTH Properties
|
||
end
|
||
-- DOC: 3.15.1 AUTH Fixed Header
|
||
local header = make_header(packet_type.AUTH, 0, variable_header:len())
|
||
return combine(header, variable_header)
|
||
end
|
||
|
||
-- Create packet of given {type: number} in args
|
||
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
|
||
return make_packet_connect(args)
|
||
elseif ptype == packet_type.PUBLISH then
|
||
return make_packet_publish(args)
|
||
elseif ptype == packet_type.PUBACK then
|
||
return make_packet_puback(args)
|
||
elseif ptype == packet_type.PUBREC then
|
||
return make_packet_pubrec(args)
|
||
elseif ptype == packet_type.PUBREL then
|
||
return make_packet_pubrel(args)
|
||
elseif ptype == packet_type.PUBCOMP then
|
||
return make_packet_pubcomp(args)
|
||
elseif ptype == packet_type.SUBSCRIBE then
|
||
return make_packet_subscribe(args)
|
||
elseif ptype == packet_type.UNSUBSCRIBE then
|
||
return make_packet_unsubscribe(args)
|
||
elseif ptype == packet_type.PINGREQ then
|
||
-- DOC: 3.12 PINGREQ – PING request
|
||
return combine("\192\000") -- 192 == 0xC0, type == 12, flags == 0
|
||
elseif ptype == packet_type.DISCONNECT then
|
||
return make_packet_disconnect(args)
|
||
elseif ptype == packet_type.AUTH then
|
||
return make_packet_auth(args)
|
||
else
|
||
error("unexpected packet type to make: "..ptype)
|
||
end
|
||
end
|
||
|
||
-- Parse properties using given read_data function for specified packet type
|
||
-- Result will be stored in packet.properties and packet.user_properties
|
||
-- Returns false plus string with error message on failure
|
||
-- Returns true on success
|
||
local function parse_properties(ptype, read_data, input, packet)
|
||
assert(type(read_data) == "function", "expecting read_data to be a function")
|
||
-- DOC: 2.2.2 Properties
|
||
-- parse Property Length
|
||
-- create read_func for parse_var_length and other parse functions, reading from data string instead of network connector
|
||
local len, err = parse_var_length(read_data)
|
||
if not len then
|
||
return false, "failed to parse properties length: "..err
|
||
end
|
||
-- check data contains enough bytes for reading properties
|
||
if input.available < len then
|
||
return false, "not enough data to parse properties of length "..len
|
||
end
|
||
-- ensure properties and user_properties are presented in packet
|
||
if not packet.properties then
|
||
packet.properties = {}
|
||
end
|
||
if not packet.user_properties then
|
||
packet.user_properties = {}
|
||
end
|
||
local uprops = packet.user_properties
|
||
-- parse allowed properties
|
||
local uprop_id = properties.user_property
|
||
local allowed = assert(allowed_properties[ptype], "no allowed properties for specified packet type: "..tostring(ptype))
|
||
local props_end = input[1] + len
|
||
while input[1] < props_end do
|
||
-- property id, DOC: 2.2.2.2 Property
|
||
local prop_id
|
||
prop_id, err = parse_var_length(read_data)
|
||
if not prop_id then
|
||
return false, "failed to parse property length: "..err
|
||
end
|
||
if not allowed[prop_id] then
|
||
return false, "property "..prop_id.." is not allowed for packet type "..ptype
|
||
end
|
||
if prop_id == uprop_id then
|
||
-- parse name=value string pair
|
||
local name, value
|
||
name, err = parse_string(read_data)
|
||
if not name then
|
||
return false, "failed to parse user property name: "..err
|
||
end
|
||
value, err = parse_string(read_data)
|
||
if not value then
|
||
return false, "failed to parse user property value: "..err
|
||
end
|
||
local old_val = uprops[name]
|
||
if old_val ~= nil then
|
||
-- ensure uprops contains pairs with name = <old value>
|
||
local found = false
|
||
for _, pair in ipairs(uprops) do
|
||
if pair[1] == name and pair[2] == old_val then
|
||
found = true
|
||
break
|
||
end
|
||
end
|
||
if not found then
|
||
uprops[#uprops + 1] = {name, old_val}
|
||
end
|
||
uprops[#uprops + 1] = {name, value}
|
||
end
|
||
uprops[name] = value
|
||
else
|
||
-- parse property value according its identifier
|
||
local value
|
||
value, err = property_parse[prop_id](read_data)
|
||
if err then
|
||
return false, "failed ro parse property "..prop_id.." value: "..err
|
||
end
|
||
if property_multiple[prop_id] then
|
||
local curr = packet.properties[properties[prop_id]] or {}
|
||
curr[#curr + 1] = value
|
||
packet.properties[properties[prop_id]] = curr
|
||
else
|
||
packet.properties[properties[prop_id]] = value
|
||
end
|
||
end
|
||
end
|
||
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
|
||
end
|
||
local byte1, byte2, err, rc, ok, packet, topic, packet_id
|
||
local read_data = input.read_func
|
||
|
||
-- parse readed 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"
|
||
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
|
||
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
|
||
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
|
||
-- 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)
|
||
ok, err = parse_properties(ptype, read_data, input, packet)
|
||
if not ok then
|
||
return false, "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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
end
|
||
-- DOC: 3.9.2.1 SUBACK Properties
|
||
packet = setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
|
||
ok, err = parse_properties(ptype, read_data, input, packet)
|
||
if not ok then
|
||
return false, "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
|
||
-- 3.11.2.1 UNSUBACK Properties
|
||
packet = setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
|
||
ok, err = parse_properties(ptype, read_data, input, packet)
|
||
if not ok then
|
||
return false, "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
|
||
if not next(rcs) then
|
||
return false, "expecting at least one reason code in UNSUBACK"
|
||
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
|
||
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
|
||
end
|
||
else
|
||
return false, "unexpected packet type received: "..tostring(ptype)
|
||
end
|
||
if input.available > 0 then
|
||
return false, "extra data in remaining length left after packet parsing"
|
||
end
|
||
return packet
|
||
end
|
||
|
||
-- export module table
|
||
return protocol5
|
||
|
||
-- vim: ts=4 sts=4 sw=4 noet ft=lua
|