1571 lines
59 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[
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 ipairs = ipairs
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 const = require("mqtt.const")
local const_v50 = const.v50
local tools = require("mqtt.tools")
local sortedpairs = tools.sortedpairs
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
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)
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, -- luacheck: ignore
parse = function(read_func_) error("not implemented") end, }, -- luacheck: ignore
{ 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] = { 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] = {
[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 sortedpairs(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")
if not allowed[prop_id] then
error("property "..name.." is not allowed for packet type "..packet_type[ptype])
end
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 = {}
local dups = {}
if args.user_properties[1] then
-- at first use array items as they given as {name, value} pairs with stable order
for i, pair in ipairs(args.user_properties) do
-- validate types for name and value
if type(pair) ~= "table" then
error(fmt("user property at position %d should be {name, value} table", i))
end
if type(pair[1]) ~= "string" then
error(fmt("user property name at position %d should be a string", i))
end
if type(pair[2]) ~= "string" then
error(fmt("user property '%s' value at position %d should be a string", pair[1], i))
end
order[i] = pair
dups[pair[1]] = pair[2]
end
end
-- now add the rest of user properties given as string table keys
for name, val in sortedpairs(args.user_properties) do
if type(name) ~= "number" then -- skipping number keys as they already added above
-- validate types for name and value
if type(name) ~= "string" then
error(fmt("user property name '%s' should be a string", name))
end
if type(val) ~= "string" then
error(fmt("user property '%s' value '%s' should be a string", name, val))
end
-- check that name+value key already added
if dups[name] ~= val then
order[#order + 1] = {name, val}
end
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 CONNACK packet, DOC: 3.2 CONNACK Connect acknowledgement
local function make_packet_connack(args)
-- check args
assert(type(args.sp) == "boolean", "expecting .sp to be a boolean with Session Present flag")
assert(type(args.rc) == "number", "expecting .rc to be a number with Connect Reason Code")
-- DOC: 3.2.2 CONNACK Variable Header
local props = make_properties(packet_type.CONNACK, args)
local variable_header = combine(
make_uint8(args.sp and 1 or 0), -- DOC: 3.2.2.1.1 Session Present
make_uint8(args.rc), -- DOC: 3.2.2.2 Connect Reason Code
props -- DOC: 3.2.2.3 CONNACK Properties
)
-- DOC: 3.2.3 CONNACK Payload
-- DOC: The CONNACK packet has no Payload.
-- DOC: 3.2.1 CONNACK Fixed Header
local header = make_header(packet_type.CONNACK, 0, variable_header:len())
return combine(header, variable_header)
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 314 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
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 318 SUBSCRIBE packet Fixed Header
return combine(header, variable_header, payload)
end
-- Create SUBACK packet, DOC: 3.9 SUBACK Subscribe acknowledgement
local function make_packet_suback(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) == "table", "expecting .rc to be a table")
assert(#args.rc > 0, "expecting .rc to be a non-empty array")
-- DOC: 3.9.2 SUBACK Variable Header
local variable_header = combine(
make_uint16(args.packet_id),
make_properties(packet_type.SUBACK, args) -- DOC: 3.9.2.1 SUBACK Properties
)
-- DOC: 3.9.3 SUBACK Payload
local payload = combine()
for i, rc in ipairs(args.rc) do
assert(type(rc) == "number", "expecting .rc["..i.."] to be a number")
assert(rc >= 0 and rc <= 255, "expecting .rc["..i.."] to be in range [0, 255]")
payload:append(make_uint8(rc))
end
-- DOC: 3.9.1 SUBACK Fixed Header
local header = make_header(packet_type.SUBACK, 0, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x0
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 UNSUBACK packet, DOC: 3.11 UNSUBACK Unsubscribe acknowledgement
local function make_packet_unsuback(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) == "table", "expecting .rc to be a table")
assert(#args.rc > 0, "expecting .rc to be a non-empty array")
-- DOC: 3.11.2 UNSUBACK Variable Header
local variable_header = combine(
make_uint16(args.packet_id),
make_properties(packet_type.UNSUBACK, args) -- DOC: 3.11.2.1 UNSUBACK Properties
)
-- DOC: 3.11.3 UNSUBACK Payload
local payload = combine()
for i, rc in ipairs(args.rc) do
assert(type(rc) == "number", "expecting .rc["..i.."] to be a number")
assert(rc >= 0 and rc <= 255, "expecting .rc["..i.."] to be in range [0, 255]")
payload:append(make_uint8(rc))
end
-- DOC: 3.11.1 UNSUBACK Fixed Header
local header = make_header(packet_type.UNSUBACK, 0, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x0
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 -- 1
return make_packet_connect(args)
elseif ptype == packet_type.CONNACK then -- 3
return make_packet_connack(args)
elseif ptype == packet_type.PUBLISH then -- 3
return make_packet_publish(args)
elseif ptype == packet_type.PUBACK then -- 4
return make_packet_puback(args)
elseif ptype == packet_type.PUBREC then -- 5
return make_packet_pubrec(args)
elseif ptype == packet_type.PUBREL then -- 6
return make_packet_pubrel(args)
elseif ptype == packet_type.PUBCOMP then -- 7
return make_packet_pubcomp(args)
elseif ptype == packet_type.SUBSCRIBE then -- 8
return make_packet_subscribe(args)
elseif ptype == packet_type.SUBACK then -- 9
return make_packet_suback(args)
elseif ptype == packet_type.UNSUBSCRIBE then -- 10
return make_packet_unsubscribe(args)
elseif ptype == packet_type.UNSUBACK then -- 11
return make_packet_unsuback(args)
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.PINGRESP then -- 12
-- DOC: 3.13 PINGRESP PING response
return combine("\208\000") -- 208 == 0xD0, type == 13, flags == 0
elseif ptype == packet_type.DISCONNECT then -- 14
return make_packet_disconnect(args)
elseif ptype == packet_type.AUTH then -- 15
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
if not allowed[prop_id] then
return false, "property "..tostring(properties[prop_id]).." ("..prop_id..") is not allowed for that packet type"
end
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 to parse property "..prop_id.." value: "..err
end
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 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 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 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 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
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, packet_type[ptype]..": failed to parse packet properties: "..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
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, packet_type[ptype]..": failed to parse packet properties: "..err
end
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
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
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
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
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
-- 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
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
-- SUBACK return codes/reason code strings
-- DOC: Table 38 - Subscribe Reason Codes
local suback_rc = {
[0x00] = "Granted QoS 0",
[0x01] = "Granted QoS 1",
[0x02] = "Granted QoS 2",
[0x80] = "Unspecified error",
[0x83] = "Implementation specific error",
[0x87] = "Not authorized",
[0x8F] = "Topic Filter invalid",
[0x91] = "Packet Identifier in use",
[0x97] = "Quota exceeded",
[0x9E] = "Shared Subscriptions not supported",
[0xA1] = "Subscription Identifiers not supported",
[0xA2] = "Wildcard Subscriptions not supported",
}
protocol5.suback_rc = suback_rc
--- Parsed SUBACK packet metatable
local suback_packet_mt = {
__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
reason_strings = function(self) -- Returns reason strings for the SUBACK packet according to its rc field
local human_readable = {}
for i, rc in ipairs(self.rc) do
local reason_string = suback_rc[rc]
if reason_string then
human_readable[i] = reason_string
else
human_readable[i] = "Unknown: "..tostring(rc)
end
end
return human_readable
end,
}
suback_packet_mt.__index = suback_packet_mt
protocol5.suback_packet_mt = suback_packet_mt
-- 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}, suback_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
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
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
-- DOC: An UNSUBSCRIBE packet with no Payload is a Protocol Error.
if input.available == 0 then
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 topic filter: "..err
end
subscriptions[#subscriptions + 1] = topic_filter
end
packet.subscriptions = subscriptions
return packet
end
-- UNSUBACK Reason Codes
-- DOC[2]: Table 39 - Unsubscribe Reason Codes
local unsuback_rc = {
[0x00] = "Success",
[0x11] = "No subscription existed",
[0x80] = "Unspecified error",
[0x83] = "Implementation specific error",
[0x87] = "Not authorized",
[0x8F] = "Topic Filter invalid",
[0x91] = "Packet Identifier in use",
}
protocol5.unsuback_rc = unsuback_rc
--- Parsed UNSUBACK packet metatable
local unsuback_packet_mt = {
__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
reason_strings = function(self) -- Returns reason strings for the UNSUBACK packet according to its rc field
local human_readable = {}
for i, rc in ipairs(self.rc) do
local reason_string = unsuback_rc[rc]
if reason_string then
human_readable[i] = reason_string
else
human_readable[i] = "Unknown: "..tostring(rc)
end
end
return human_readable
end,
}
unsuback_packet_mt.__index = unsuback_packet_mt
protocol5.unsuback_packet_mt = unsuback_packet_mt
-- 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}, unsuback_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
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, _)
-- 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, _)
-- 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
-- DISCONNECT reason codes
-- DOC: Table 310 Disconnect Reason Code values
local disconnect_rc = {
[0x00] = "Normal disconnection",
[0x04] = "Disconnect with Will Message",
[0x80] = "Unspecified error",
[0x81] = "Malformed Packet",
[0x82] = "Protocol Error",
[0x83] = "Implementation specific error",
[0x87] = "Not authorized",
[0x89] = "Server busy",
[0x8B] = "Server shutting down",
[0x8D] = "Keep Alive timeout",
[0x8E] = "Session taken over",
[0x8F] = "Topic Filter invalid",
[0x90] = "Topic Name invalid",
[0x93] = "Receive Maximum exceeded",
[0x94] = "Topic Alias invalid",
[0x95] = "Packet too large",
[0x96] = "Message rate too high",
[0x97] = "Quota exceeded",
[0x98] = "Administrative action",
[0x99] = "Payload format invalid",
[0x9A] = "Retain not supported",
[0x9B] = "QoS not supported",
[0x9C] = "Use another server",
[0x9D] = "Server moved",
[0x9E] = "Shared Subscriptions not supported",
[0x9F] = "Connection rate exceeded",
[0xA0] = "Maximum connect time",
[0xA1] = "Subscription Identifiers not supported",
[0xA2] = "Wildcard Subscriptions not supported",
}
protocol5.disconnect_rc = disconnect_rc
--- Parsed DISCONNECT packet metatable
local disconnect_packet_mt = {
__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
reason_string = function(self) -- Returns reason string for the DISCONNECT packet according to its rc field
local reason_string = disconnect_rc[self.rc]
if not reason_string then
reason_string = "Unknown: "..self.rc
end
return reason_string
end,
}
disconnect_packet_mt.__index = disconnect_packet_mt
protocol5.disconnect_packet_mt = disconnect_packet_mt
-- 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={}}, disconnect_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)
if not rc then
return false, packet_type[ptype]..": failed to parse rc: "..err
end
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, packet_type[ptype]..": failed to parse packet properties: "..err
end
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
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, packet_type[ptype]..": failed to parse packet properties: "..err
end
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.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
-- 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
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
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
packet.username = nil
end
-- 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 setmetatable(packet, packet_mt)
end
-- export module table
return protocol5
-- vim: ts=4 sts=4 sw=4 noet ft=lua