1040 lines
40 KiB
Lua
Raw 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 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 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 -- 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 318 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