2676 lines
75 KiB
Lua
2676 lines
75 KiB
Lua
local comm = require "comm"
|
|
local json = require "json"
|
|
local lpeg = require "lpeg"
|
|
local math = require "math"
|
|
local nmap = require "nmap"
|
|
local stdnse = require "stdnse"
|
|
local string = require "string"
|
|
local stringaux = require "stringaux"
|
|
local table = require "table"
|
|
local unittest = require "unittest"
|
|
|
|
_ENV = stdnse.module("coap", stdnse.seeall)
|
|
|
|
---
|
|
-- An implementation of CoAP
|
|
-- https://tools.ietf.org/html/rfc7252
|
|
--
|
|
-- This library does not currently implement the entire CoAP protocol,
|
|
-- only those behaviours which are necessary for existing scripts are
|
|
-- included. Extending to accommodate additional control packets should
|
|
-- not be difficult.
|
|
--
|
|
-- @author "Mak Kolybabi <mak@kolybabi.com>"
|
|
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
|
|
|
|
COAP = {}
|
|
|
|
COAP.build = nil
|
|
COAP.parse = nil
|
|
|
|
COAP.header = {}
|
|
COAP.header.build = nil
|
|
COAP.header.parse = nil
|
|
|
|
COAP.header.codes = {}
|
|
COAP.header.codes.build = nil
|
|
COAP.header.codes.parse = nil
|
|
|
|
COAP.header.options = {}
|
|
COAP.header.options.build = nil
|
|
COAP.header.options.parse = nil
|
|
|
|
COAP.header.options.delta_length = {}
|
|
COAP.header.options.delta_length.build = nil
|
|
COAP.header.options.delta_length.parse = nil
|
|
|
|
COAP.header.options.accept = {}
|
|
COAP.header.options.accept.build = nil
|
|
COAP.header.options.accept.parse = nil
|
|
|
|
COAP.header.options.block1 = {}
|
|
COAP.header.options.block1.build = nil
|
|
COAP.header.options.block1.parse = nil
|
|
|
|
COAP.header.options.block2 = {}
|
|
COAP.header.options.block2.build = nil
|
|
COAP.header.options.block2.parse = nil
|
|
|
|
COAP.header.options.content_format = {}
|
|
COAP.header.options.content_format.build = nil
|
|
COAP.header.options.content_format.parse = nil
|
|
|
|
COAP.header.options.etag = {}
|
|
COAP.header.options.etag.build = nil
|
|
COAP.header.options.etag.parse = nil
|
|
|
|
COAP.header.options.if_match = {}
|
|
COAP.header.options.if_match.build = nil
|
|
COAP.header.options.if_match.parse = nil
|
|
|
|
COAP.header.options.if_none_match = {}
|
|
COAP.header.options.if_none_match.build = nil
|
|
COAP.header.options.if_none_match.parse = nil
|
|
|
|
COAP.header.options.location_path = {}
|
|
COAP.header.options.location_path.build = nil
|
|
COAP.header.options.location_path.parse = nil
|
|
|
|
COAP.header.options.location_query = {}
|
|
COAP.header.options.location_query.build = nil
|
|
COAP.header.options.location_query.parse = nil
|
|
|
|
COAP.header.options.max_age = {}
|
|
COAP.header.options.max_age.build = nil
|
|
COAP.header.options.max_age.parse = nil
|
|
|
|
COAP.header.options.proxy_scheme = {}
|
|
COAP.header.options.proxy_scheme.build = nil
|
|
COAP.header.options.proxy_scheme.parse = nil
|
|
|
|
COAP.header.options.proxy_uri = {}
|
|
COAP.header.options.proxy_uri.build = nil
|
|
COAP.header.options.proxy_uri.parse = nil
|
|
|
|
COAP.header.options.size1 = {}
|
|
COAP.header.options.size1.build = nil
|
|
COAP.header.options.size1.parse = nil
|
|
|
|
COAP.header.options.uri_host = {}
|
|
COAP.header.options.uri_host.build = nil
|
|
COAP.header.options.uri_host.parse = nil
|
|
|
|
COAP.header.options.uri_path = {}
|
|
COAP.header.options.uri_path.build = nil
|
|
COAP.header.options.uri_path.parse = nil
|
|
|
|
COAP.header.options.uri_port = {}
|
|
COAP.header.options.uri_port.build = nil
|
|
COAP.header.options.uri_port.parse = nil
|
|
|
|
COAP.header.options.uri_query = {}
|
|
COAP.header.options.uri_query.build = nil
|
|
COAP.header.options.uri_query.parse = nil
|
|
|
|
COAP.header.options.value = {}
|
|
|
|
COAP.header.options.value.block = {}
|
|
COAP.header.options.value.block.build = nil
|
|
COAP.header.options.value.block.parse = nil
|
|
|
|
COAP.header.options.value.empty = {}
|
|
COAP.header.options.value.empty.build = nil
|
|
COAP.header.options.value.empty.parse = nil
|
|
|
|
COAP.header.options.value.opaque = {}
|
|
COAP.header.options.value.opaque.build = nil
|
|
COAP.header.options.value.opaque.parse = nil
|
|
|
|
COAP.header.options.value.uint = {}
|
|
COAP.header.options.value.uint.build = nil
|
|
COAP.header.options.value.uint.parse = nil
|
|
|
|
COAP.header.options.value.string = {}
|
|
COAP.header.options.value.string.build = nil
|
|
COAP.header.options.value.string.parse = nil
|
|
|
|
COAP.header.find_option = nil
|
|
COAP.header.find_options = nil
|
|
|
|
COAP.payload = {}
|
|
COAP.payload.parse = nil
|
|
|
|
COAP.payload.text_plain = {}
|
|
COAP.payload.text_plain.build = nil
|
|
COAP.payload.text_plain.parse = nil
|
|
|
|
COAP.payload.application_link_format = {}
|
|
COAP.payload.application_link_format.build = nil
|
|
COAP.payload.application_link_format.parse = nil
|
|
|
|
COAP.payload.application_xml = {}
|
|
COAP.payload.application_xml.build = nil
|
|
COAP.payload.application_xml.parse = nil
|
|
|
|
COAP.payload.application_octet_stream = {}
|
|
COAP.payload.application_octet_stream.build = nil
|
|
COAP.payload.application_octet_stream.parse = nil
|
|
|
|
COAP.payload.application_exi = {}
|
|
COAP.payload.application_exi.build = nil
|
|
COAP.payload.application_exi.parse = nil
|
|
|
|
COAP.payload.application_json = {}
|
|
COAP.payload.application_json.build = nil
|
|
COAP.payload.application_json.parse = nil
|
|
|
|
--- Builds a CoAP message.
|
|
--
|
|
-- @name COAP.build
|
|
--
|
|
-- @param options Table of options accepted by the desired message
|
|
-- build function.
|
|
-- @param payload String representing the message payload.
|
|
--
|
|
-- @return status true on success, false on failure.
|
|
-- @return response String representing a raw message on success, or
|
|
-- containing the error message on failure.
|
|
COAP.build = function(options, payload)
|
|
-- Sanity check the payload.
|
|
if not payload then
|
|
payload = ""
|
|
end
|
|
assert(type(payload) == "string")
|
|
|
|
assert(type(options) == "table")
|
|
|
|
-- Build the header.
|
|
local pkt = COAP.header.build(options)
|
|
|
|
-- Build the payload.
|
|
if payload ~= "" then
|
|
pkt = pkt .. string.char(0xFF)
|
|
end
|
|
pkt = pkt .. COAP.payload.build(options, payload)
|
|
|
|
return pkt
|
|
end
|
|
|
|
--- Parses a CoAP message.
|
|
--
|
|
-- @name COAP.parse
|
|
--
|
|
-- @param buf String from which to parse the message.
|
|
-- @param pos Position from which to start parsing.
|
|
--
|
|
-- @return pos String index on success, false on failure.
|
|
-- @return response Table representing a message on success, string
|
|
-- containing the error message on failure.
|
|
COAP.parse = function(buf, pos)
|
|
assert(type(buf) == "string")
|
|
|
|
if not pos or pos == 0 then
|
|
pos = 1
|
|
end
|
|
assert(type(pos) == "number")
|
|
assert(pos <= #buf)
|
|
|
|
-- Parse the fixed header.
|
|
local pos, hdr = COAP.header.parse(buf, pos)
|
|
if not pos then
|
|
return false, hdr
|
|
end
|
|
|
|
-- If we've reached the end of the packet, there's no payload and we
|
|
-- can return immediately.
|
|
if pos > #buf then
|
|
return pos, hdr
|
|
end
|
|
|
|
-- If we're not at the end of the buffer, but the next byte after
|
|
-- the header and options is not the payload marker, return
|
|
-- immediately. We've got no idea what we're looking at.
|
|
if buf:byte(pos) ~= 0xFF then
|
|
stdnse.debug3("Parsed to byte %d of %d of packet, remaining bytes not understood.", pos - 1, #buf)
|
|
return pos, hdr
|
|
end
|
|
pos = pos + 1
|
|
|
|
-- If there's nothing past the payload marker, which is how some
|
|
-- implementations format their packets.
|
|
if pos > #buf then
|
|
return pos, hdr
|
|
end
|
|
|
|
-- By this point, we have the payload and it's prefixed by the
|
|
-- payload marker. We know this is a payload, so extract it.
|
|
local payload = buf:sub(pos)
|
|
pos = #buf + 1
|
|
|
|
-- If the header contains a block options, then we can't parse the
|
|
-- payload since it spans multiple packets, so we return it raw.
|
|
local b1opt = COAP.header.find_option(hdr, "block1")
|
|
local b2opt = COAP.header.find_option(hdr, "block2")
|
|
if b1opt or b2opt then
|
|
hdr.payload = payload
|
|
return pos, hdr
|
|
end
|
|
|
|
-- In the absence of block options, we should be able to parse the
|
|
-- payload.
|
|
local status, payload = COAP.payload.parse(hdr, payload)
|
|
if not status then
|
|
return false, payload
|
|
end
|
|
hdr.payload = payload
|
|
|
|
return pos, hdr
|
|
end
|
|
|
|
COAP.header.types = {
|
|
["confirmable"] = 0,
|
|
["non-confirmable"] = 1,
|
|
["acknowledgement"] = 2,
|
|
["reset"] = 3,
|
|
}
|
|
|
|
--- Builds a CoAP message header.
|
|
--
|
|
-- @name COAP.header.build
|
|
--
|
|
-- See section "3. Message Format" of the standard.
|
|
--
|
|
-- @param options Table of options accepted by the desired message
|
|
-- build function.
|
|
--
|
|
-- @return status true on success, false on failure.
|
|
-- @return response String representing a raw message header on
|
|
-- success, or containing the error message on failure.
|
|
COAP.header.build = function(options)
|
|
assert(type(options) == "table")
|
|
|
|
-- Fields which can be left as default.
|
|
local ver = options.version
|
|
if not ver then
|
|
ver = 1
|
|
end
|
|
assert(type(ver) == "number")
|
|
assert(ver >= 0)
|
|
assert(ver <= 3)
|
|
|
|
local token = options.token
|
|
if not token then
|
|
token = ""
|
|
end
|
|
assert(type(token) == "string")
|
|
|
|
local tkl = #token
|
|
assert(type(tkl) == "number")
|
|
assert(tkl >= 0)
|
|
assert(tkl <= 8)
|
|
|
|
local id = options.id
|
|
if not id then
|
|
id = math.random(65535)
|
|
end
|
|
assert(type(id) == "number")
|
|
assert(id >= 0)
|
|
assert(id <= 65535)
|
|
|
|
-- Fields which need to be explicitly set.
|
|
local mtype = options.type
|
|
assert(type(mtype) == "string")
|
|
mtype = COAP.header.types[mtype]
|
|
assert(mtype)
|
|
|
|
local code = options.code
|
|
assert(code)
|
|
assert(type(code) == "string")
|
|
code = COAP.header.codes.build(code)
|
|
|
|
-- Build the fixed portion of the header.
|
|
|
|
ver = ver << 6
|
|
mtype = mtype << 4
|
|
|
|
local pkt = {
|
|
string.pack("B", ver | mtype | tkl),
|
|
code,
|
|
string.pack(">I2", id),
|
|
token,
|
|
}
|
|
|
|
-- Include optional portions of the header.
|
|
if options["options"] then
|
|
pkt[#pkt+1] = COAP.header.options.build(options.options)
|
|
end
|
|
|
|
return table.concat(pkt)
|
|
end
|
|
|
|
--- Parses a CoAP message header.
|
|
--
|
|
-- @name COAP.header.parse
|
|
--
|
|
-- See section "3. Message Format" of the standard.
|
|
--
|
|
-- @param buf String from which to parse the header.
|
|
-- @param pos Position from which to start parsing.
|
|
--
|
|
-- @return pos String index on success, false on failure.
|
|
-- @return response Table representing a message header on success,
|
|
-- string containing the error message on failure.
|
|
COAP.header.parse = function(buf, pos)
|
|
assert(type(buf) == "string")
|
|
|
|
if not pos or pos == 0 then
|
|
pos = 1
|
|
end
|
|
assert(type(pos) == "number")
|
|
assert(pos <= #buf)
|
|
|
|
if #buf - pos + 1 < 4 then
|
|
return false, "Fixed header extends past end of buffer."
|
|
end
|
|
|
|
local ver_type_tkl, code, id, pos = string.unpack(">Bc1I2", buf, pos)
|
|
|
|
-- Parse the fixed header.
|
|
local hdr = {}
|
|
|
|
local ver = ver_type_tkl >> 6
|
|
hdr.version = ver
|
|
|
|
local mtype = ver_type_tkl >> 4
|
|
mtype = mtype & 0x3
|
|
|
|
hdr.type = ("(unrecognized: %d)"):format(mtype)
|
|
for key, val in pairs(COAP.header.types) do
|
|
if val == mtype then
|
|
hdr.type = key
|
|
break
|
|
end
|
|
end
|
|
|
|
local tkl = ver_type_tkl & 0xF
|
|
if tkl < 0 or tkl > 8 then
|
|
return false, ("Token length was %d, but must be 0 through 8."):format(tkl)
|
|
end
|
|
hdr.token_length = tkl
|
|
|
|
local status, code = COAP.header.codes.parse(code)
|
|
if not status then
|
|
return false, code
|
|
end
|
|
hdr.code = code
|
|
|
|
hdr.id = id
|
|
|
|
-- The token can be between 0 and 8 bytes.
|
|
if hdr.token_length > 0 then
|
|
hdr.token = buf:sub(pos, pos + hdr.token_length - 1)
|
|
pos = pos + hdr.token_length
|
|
end
|
|
|
|
-- If we've reached the end of the packet, there's no options or
|
|
-- payload and we can return immediately after we put in an empty
|
|
-- options table.
|
|
if pos > #buf then
|
|
hdr.options = {}
|
|
return pos, hdr
|
|
end
|
|
|
|
-- Parse the options.
|
|
local pos, opt = COAP.header.options.parse(buf, pos)
|
|
if not pos then
|
|
return false, opt
|
|
end
|
|
hdr.options = opt
|
|
|
|
return pos, hdr
|
|
end
|
|
|
|
COAP.header.codes.ids = {
|
|
-- Requests
|
|
["get"] = {0, 1},
|
|
["post"] = {0, 2},
|
|
["put"] = {0, 3},
|
|
["delete"] = {0, 4},
|
|
|
|
-- Responses
|
|
["created"] = {2, 1},
|
|
["deleted"] = {2, 2},
|
|
["valid"] = {2, 3},
|
|
["changed"] = {2, 4},
|
|
["content"] = {2, 5},
|
|
["bad_request"] = {4, 0},
|
|
["unauthorized"] = {4, 1},
|
|
["bad_option"] = {4, 2},
|
|
["forbidden"] = {4, 3},
|
|
["not_found"] = {4, 4},
|
|
["method_not_allowed"] = {4, 5},
|
|
["not_acceptable"] = {4, 6},
|
|
["precondition_failed"] = {4, 12},
|
|
["request_entity_too_large"] = {4, 13},
|
|
["unsupported_content-format"] = {4, 15},
|
|
["internal_server_error"] = {5, 0},
|
|
["not_implemented"] = {5, 1},
|
|
["bad_gateway"] = {5, 2},
|
|
["service_unavailable"] = {5, 3},
|
|
["gateway_timeout"] = {5, 4},
|
|
["proxying_not_supported"] = {5, 5},
|
|
}
|
|
|
|
--- Builds a CoAP message request or response code.
|
|
--
|
|
-- @name COAP.header.codes.build
|
|
--
|
|
-- @param name String naming the desired code.
|
|
--
|
|
-- @return status true on success, false on failure.
|
|
-- @return response String representing a code on success, or
|
|
-- containing the error message on failure.
|
|
COAP.header.codes.build = function(name)
|
|
assert(type(name) == "string")
|
|
|
|
local id = COAP.header.codes.ids[name]
|
|
assert(id, ("Code '%s' not recognized."):format(name))
|
|
|
|
local class = id[1]
|
|
local detail = id[2]
|
|
|
|
class = class << 5
|
|
|
|
return string.pack("B", class | detail)
|
|
end
|
|
|
|
--- Parses a CoAP request or response code.
|
|
--
|
|
-- @name COAP.header.codes.parse
|
|
--
|
|
-- @param buf String from which to parse the code.
|
|
-- @param pos Position from which to start parsing.
|
|
--
|
|
-- @return pos String index on success, false on failure.
|
|
-- @return response Table representing the code on success, string
|
|
-- containing the error message on failure.
|
|
COAP.header.codes.parse = function(buf, pos)
|
|
assert(type(buf) == "string")
|
|
if #buf < 1 then
|
|
return false, "Cannot parse a string of less than one byte."
|
|
end
|
|
|
|
if not pos or pos == 0 then
|
|
pos = 1
|
|
end
|
|
assert(type(pos) == "number")
|
|
assert(pos <= #buf)
|
|
|
|
local id, pos = string.unpack("B", buf, pos)
|
|
if not pos then
|
|
return false, id
|
|
end
|
|
|
|
local class = id >> 5
|
|
local detail = id & 0x1F
|
|
|
|
for key, val in pairs(COAP.header.codes.ids) do
|
|
if val[1] == class and val[2] == detail then
|
|
return pos, key
|
|
end
|
|
end
|
|
|
|
return false, ("Code '%d.%02d' not recognized."):format(class, detail)
|
|
end
|
|
|
|
COAP.header.options.ids = {
|
|
["if_match"] = 1,
|
|
["uri_host"] = 3,
|
|
["etag"] = 4,
|
|
["if_none_match"] = 5,
|
|
["uri_port"] = 7,
|
|
["location_path"] = 8,
|
|
["uri_path"] = 11,
|
|
["content_format"] = 12,
|
|
["max_age"] = 14,
|
|
["uri_query"] = 15,
|
|
["accept"] = 17,
|
|
["location_query"] = 20,
|
|
["block2"] = 23,
|
|
["block1"] = 27,
|
|
["proxy_uri"] = 35,
|
|
["proxy_scheme"] = 39,
|
|
["size1"] = 60,
|
|
}
|
|
|
|
--- Build CoAP message header options.
|
|
--
|
|
-- @name COAP.header.options.build
|
|
--
|
|
-- See section "3.1. Option Format" of the standard.
|
|
--
|
|
-- Due to the ordering of options and using delta representation of
|
|
-- their identifiers, we process all options at once.
|
|
--
|
|
-- The sorting method used is in this function is terrible, but using
|
|
-- Lua's sort with a function gave seemingly inconsistent results. We
|
|
-- have rolled-our-own stable sort which functions properly. Replacing
|
|
-- it is welcome.
|
|
--
|
|
-- @param options Table of options and their values.
|
|
--
|
|
-- @return response String representing a raw set of options, properly
|
|
-- sorted.
|
|
COAP.header.options.build = function(options)
|
|
-- Sanity check the option table.
|
|
assert(type(options) == "table")
|
|
if #options == 0 then
|
|
return ""
|
|
end
|
|
|
|
-- Each option needs to have an ID, since that's used for ordering
|
|
-- and the delta value.
|
|
local ids = {}
|
|
for _, opt in pairs(options) do
|
|
local id = COAP.header.options.ids[opt.name]
|
|
assert(id)
|
|
opt.id = id
|
|
ids[id] = true
|
|
end
|
|
|
|
-- Options are encoded in order of their corresponding IDs, and
|
|
-- contain a delta value indicating the offset of the option's ID
|
|
-- from the previous option, which allows gaps.
|
|
--
|
|
-- We start by ordering the array of options, using stable sorting
|
|
-- so that duplicate options retain their relative ordering. The
|
|
-- range of IDs is large enough to warrant sorting instead of
|
|
-- iterating through all possibilities.
|
|
local unique_ids = {}
|
|
for key, val in pairs(ids) do
|
|
table.insert(unique_ids, key)
|
|
end
|
|
|
|
table.sort(unique_ids)
|
|
|
|
local sorted_options = {}
|
|
for _, id in ipairs(unique_ids) do
|
|
for _, opt in pairs(options) do
|
|
if opt.id == id then
|
|
table.insert(sorted_options, opt)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The first option, and duplicate instances of an option, can be
|
|
-- encoded using a delta of zero.
|
|
local prev = 0
|
|
|
|
local pkt = ""
|
|
for _, opt in ipairs(sorted_options) do
|
|
-- Build the option's value.
|
|
local val = COAP.header.options[opt.name].build(opt.value)
|
|
|
|
-- Calculate delta of this option's ID versus the previous
|
|
-- option's ID.
|
|
local delta = opt.id - prev
|
|
assert(delta >= 0)
|
|
prev = opt.id
|
|
|
|
-- We must delete the ID key from the option to prevent it from
|
|
-- persisting on the shared object that was passed in, which can
|
|
-- bungle our tests.
|
|
opt.id = nil
|
|
|
|
-- Due to the complex nature of the delta and length fields, they
|
|
-- are handled together.
|
|
local delta_and_length = COAP.header.options.delta_length.build(delta, #val)
|
|
|
|
pkt = pkt .. delta_and_length .. val
|
|
end
|
|
|
|
return pkt
|
|
end
|
|
|
|
--- Parses a CoAP message's header options.
|
|
--
|
|
-- @name COAP.header.options.parse
|
|
--
|
|
-- See section "3.1. Option Format" of the standard.
|
|
--
|
|
-- @param buf String from which to parse the options.
|
|
-- @param pos Position from which to start parsing.
|
|
--
|
|
-- @return pos String index on success, false on failure.
|
|
-- @return response Table representing options on success, string
|
|
-- containing the error message on failure.
|
|
COAP.header.options.parse = function(buf, pos)
|
|
assert(type(buf) == "string")
|
|
if #buf < 1 then
|
|
return false, nil, nil, "Cannot parse a string of less than one byte."
|
|
end
|
|
|
|
if not pos or pos == 0 then
|
|
pos = 1
|
|
end
|
|
assert(type(pos) == "number")
|
|
assert(pos <= #buf, ("pos<%d> <= #buf<%d>"):format(pos, #buf))
|
|
|
|
local prev = 0
|
|
local options = {}
|
|
while pos <= #buf do
|
|
-- Check for the Packet Marker which terminates the options list.
|
|
if buf:byte(pos) == 0xFF then
|
|
break
|
|
end
|
|
|
|
-- Parse the first one to five bytes of the option.
|
|
local delta, err, length
|
|
pos, delta, length, err = COAP.header.options.delta_length.parse(buf, pos)
|
|
if not pos then
|
|
return false, err
|
|
end
|
|
|
|
-- Reconstruct the ID and name of the option.
|
|
local id = prev + delta
|
|
prev = id
|
|
local name = nil
|
|
for key, val in pairs(COAP.header.options.ids) do
|
|
if val == id then
|
|
name = key
|
|
break
|
|
end
|
|
end
|
|
|
|
-- XXX-MAK: Technically, we should determine whether the option is
|
|
-- critical and only fail if it is. However, this works well
|
|
-- enough.
|
|
if not name then
|
|
return false, ("Failed to find name for option with ID %d."):format(id)
|
|
end
|
|
|
|
-- Extract the value bytes from the buffer, since the option value
|
|
-- parsers cannot determine the value length on their own.
|
|
local end_pos = pos + length
|
|
if end_pos - 1 > #buf then
|
|
return false, "Option value extends past end of buffer."
|
|
end
|
|
local body = buf:sub(pos, end_pos - 1)
|
|
pos = end_pos
|
|
|
|
-- Parse the value of the option.
|
|
local val = COAP.header.options[name].parse(body)
|
|
|
|
-- Create the option definition and add it to our list.
|
|
table.insert(options, {["name"] = name, ["value"] = val})
|
|
end
|
|
|
|
return pos, options
|
|
end
|
|
|
|
--- Builds a CoAP message header Accept option.
|
|
--
|
|
-- @name COAP.header.options.accept.build
|
|
--
|
|
-- 5.10.4. Accept
|
|
--
|
|
-- @param val Number representing an acceptable content type.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.accept.build = function(val)
|
|
assert(val >= 0)
|
|
assert(val <= 65535)
|
|
return COAP.header.options.value.uint.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Accept option.
|
|
--
|
|
-- @name COAP.header.options.accept.parse
|
|
--
|
|
-- 5.10.4. Accept
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val Number representing the option's value.
|
|
COAP.header.options.accept.parse = function(buf)
|
|
return COAP.header.options.value.uint.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Block1 option.
|
|
--
|
|
-- @name COAP.header.options.block1.build
|
|
--
|
|
-- https://tools.ietf.org/html/draft-ietf-core-block-19
|
|
--
|
|
-- @see COAP.header.options.block.build
|
|
--
|
|
-- @param val Table representing the option's parameters.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.block1.build = function(val)
|
|
return COAP.header.options.value.block.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Block1 option.
|
|
--
|
|
-- @name COAP.header.options.block1.parse
|
|
--
|
|
-- https://tools.ietf.org/html/draft-ietf-core-block-19
|
|
--
|
|
-- @see COAP.header.options.block.parse
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return response Table representing the option's value.
|
|
COAP.header.options.block1.parse = function(buf)
|
|
return COAP.header.options.value.block.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Block2 option.
|
|
--
|
|
-- @name COAP.header.options.block2.build
|
|
--
|
|
-- https://tools.ietf.org/html/draft-ietf-core-block-19
|
|
--
|
|
-- @see COAP.header.options.block.build
|
|
--
|
|
-- @param val Table representing the option's parameters.
|
|
--
|
|
-- @return str String representing the option.
|
|
COAP.header.options.block2.build = function(val)
|
|
return COAP.header.options.value.block.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Block2 option.
|
|
--
|
|
-- @name COAP.header.options.block2.parse
|
|
--
|
|
-- https://tools.ietf.org/html/draft-ietf-core-block-19
|
|
--
|
|
-- @see COAP.header.options.block.parse
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return response Table representing the option's value.
|
|
COAP.header.options.block2.parse = function(buf)
|
|
return COAP.header.options.value.block.parse(buf)
|
|
end
|
|
|
|
-- The default content format, "charset=utf-8", is represented by the
|
|
-- absence of this option.
|
|
COAP.header.options.content_format.values = {
|
|
["text/plain"] = 0,
|
|
["application/link-format"] = 40,
|
|
["application/xml"] = 41,
|
|
["application/octet-stream"] = 42,
|
|
["application/exi"] = 47,
|
|
["application/json"] = 50,
|
|
}
|
|
|
|
--- Builds a CoAP message header Content-Format option.
|
|
--
|
|
-- @name COAP.header.options.content_format.build
|
|
--
|
|
-- 5.10.3. Content-Format
|
|
--
|
|
-- @param val Number representing the payload content format.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.content_format.build = function(val)
|
|
-- Translate string to number if necessary.
|
|
if type(val) == "string" then
|
|
val = COAP.headers.options.content_format.values[val]
|
|
end
|
|
assert(val)
|
|
|
|
return COAP.header.options.value.uint.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Content-Format option.
|
|
--
|
|
-- @name COAP.header.options.content_format.parse
|
|
--
|
|
-- 5.10.3. Content-Format
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.content_format.parse = function(buf)
|
|
local val = COAP.header.options.value.uint.parse(buf)
|
|
|
|
-- Translate number to string if possible.
|
|
for name, num in pairs(COAP.header.options.content_format.values) do
|
|
if num == val then
|
|
return name
|
|
end
|
|
end
|
|
|
|
return val
|
|
end
|
|
|
|
--- Builds a CoAP message header ETag option.
|
|
--
|
|
-- @name COAP.header.options.etag.build
|
|
--
|
|
-- 5.10.6. ETag
|
|
-- 5.10.6.1. ETag as a Response Option
|
|
-- 5.10.6.2. ETag as a Request Option
|
|
--
|
|
-- @param val String representing the ETag's value.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.etag.build = function(val)
|
|
assert(#val >= 1)
|
|
assert(#val <= 8)
|
|
return COAP.header.options.value.opaque.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header ETag option.
|
|
--
|
|
-- @name COAP.header.options.etag.parse
|
|
--
|
|
-- 5.10.6. ETag
|
|
-- 5.10.6.1. ETag as a Response Option
|
|
-- 5.10.6.2. ETag as a Request Option
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.etag.parse = function(buf)
|
|
return COAP.header.options.value.opaque.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header If-Match option.
|
|
--
|
|
-- @name COAP.header.options.if_match.build
|
|
--
|
|
-- 5.10.8. Conditional Request Options
|
|
-- 5.10.8.1. If-Match
|
|
--
|
|
-- @param val String representing the condition.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.if_match.build = function(val)
|
|
assert(#val >= 0)
|
|
assert(#val <= 8)
|
|
return COAP.header.options.value.opaque.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header If-Match option.
|
|
--
|
|
-- @name COAP.header.options.if_match.parse
|
|
--
|
|
-- 5.10.8. Conditional Request Options
|
|
-- 5.10.8.1. If-Match
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.if_match.parse = function(buf)
|
|
return COAP.header.options.value.opaque.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header If-None-Match option.
|
|
--
|
|
-- @name COAP.header.options.if_none_match.build
|
|
--
|
|
-- 5.10.8. Conditional Request Options
|
|
-- 5.10.8.2. If-None-Match
|
|
--
|
|
-- @param val Parameter is ignored, existing only to keep API
|
|
-- consistent.
|
|
--
|
|
-- @return str Empty string to keep API consistent.
|
|
COAP.header.options.if_none_match.build = function(val)
|
|
return COAP.header.options.value.empty.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header If-None-Match option.
|
|
--
|
|
-- @name COAP.header.options.if_none_match.parse
|
|
--
|
|
-- 5.10.8. Conditional Request Options
|
|
-- 5.10.8.2. If-None-Match
|
|
--
|
|
-- @param buf Parameter is ignored, existing only to keep API
|
|
-- consistent.
|
|
--
|
|
-- @return val Nil due to the option being empty.
|
|
COAP.header.options.if_none_match.parse = function(buf)
|
|
return COAP.header.options.value.empty.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Location-Path option.
|
|
--
|
|
-- @name COAP.header.options.location_path.build
|
|
--
|
|
-- 5.10.7. Location-Path and Location-Query
|
|
--
|
|
-- @param val String representing a path.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.location_path.build = function(val)
|
|
assert(#val >= 0)
|
|
assert(#val <= 255)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Location-Path option.
|
|
--
|
|
-- @name COAP.header.options.location_path.parse
|
|
--
|
|
-- 5.10.7. Location-Path and Location-Query
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.location_path.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Location-Query option.
|
|
--
|
|
-- @name COAP.header.options.location_query.build
|
|
--
|
|
-- 5.10.7. Location-Path and Location-Query
|
|
--
|
|
-- @param val String representing the query.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.location_query.build = function(val)
|
|
assert(#val >= 0)
|
|
assert(#val <= 255)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Location-Query option.
|
|
--
|
|
-- @name COAP.header.options.location_query.parse
|
|
--
|
|
-- 5.10.7. Location-Path and Location-Query
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.location_query.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Max-Age option.
|
|
--
|
|
-- @name COAP.header.options.max_age.build
|
|
--
|
|
-- 5.10.5. Max-Age
|
|
--
|
|
-- @param val Number representing the maximum age.
|
|
--
|
|
-- @return str String representing the option's value
|
|
COAP.header.options.max_age.build = function(val)
|
|
return COAP.header.options.value.uint.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Max-Age option.
|
|
--
|
|
-- @name COAP.header.options.max_age.parse
|
|
--
|
|
-- 5.10.5. Max-Age
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val Number representing the option's value.
|
|
COAP.header.options.max_age.parse = function(buf)
|
|
return COAP.header.options.value.uint.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Proxy-Scheme option.
|
|
--
|
|
-- @name COAP.header.options.proxy_scheme.build
|
|
--
|
|
-- 5.10.2. Proxy-Uri and Proxy-Scheme
|
|
--
|
|
-- @param val String representing the proxy scheme.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.proxy_scheme.build = function(val)
|
|
assert(#val >= 1)
|
|
assert(#val <= 255)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Proxy-Scheme option.
|
|
--
|
|
-- @name COAP.header.options.proxy_scheme.parse
|
|
--
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.proxy_scheme.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Proxy-Uri option.
|
|
--
|
|
-- @name COAP.header.options.proxy_uri.build
|
|
--
|
|
-- 5.10.2. Proxy-Uri and Proxy-Scheme
|
|
--
|
|
-- @param val String representing the proxy URI.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.proxy_uri.build = function(val)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Proxy-Uri option.
|
|
--
|
|
-- @name COAP.header.options.proxy_uri.parse
|
|
--
|
|
-- 5.10.2. Proxy-Uri and Proxy-Scheme
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.proxy_uri.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Size1 option.
|
|
--
|
|
-- @name COAP.header.options.Size1.build
|
|
--
|
|
-- 5.10.9. Size1 Option
|
|
--
|
|
-- @param val Number representing a size.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.size1.build = function(val)
|
|
return COAP.header.options.value.uint.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Size1 option.
|
|
--
|
|
-- @name COAP.header.options.size1.parse
|
|
--
|
|
-- 5.10.9. Size1 Option
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val Number representing the option's value.
|
|
COAP.header.options.size1.parse = function(buf)
|
|
return COAP.header.options.value.uint.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Uri-Host option.
|
|
--
|
|
-- @name COAP.header.options.uri_host.build
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param val String representing the host of the URI.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.uri_host.build = function(val)
|
|
assert(#val >= 1)
|
|
assert(#val <= 255)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Uri-Host option.
|
|
--
|
|
-- @name COAP.header.options.uri_host.parse
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.uri_host.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Uri-Path option.
|
|
--
|
|
-- @name COAP.header.options.uri_path.build
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param val String representing a path in the URI.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.uri_path.build = function(val)
|
|
assert(#val >= 0)
|
|
assert(#val <= 255)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Uri-Path option.
|
|
--
|
|
-- @name COAP.header.options.uri_path.parse
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.uri_path.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Uri-Port option.
|
|
--
|
|
-- @name COAP.header.options.uri_port.build
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param val Number representing an endpoint's port number.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.uri_port.build = function(val)
|
|
assert(val >= 0)
|
|
assert(val <= 65535)
|
|
return COAP.header.options.value.uint.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Uri-Port option.
|
|
--
|
|
-- @name COAP.header.options.uri_port.parse
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val Number representing the option's value.
|
|
COAP.header.options.uri_port.parse = function(buf)
|
|
return COAP.header.options.value.uint.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Uri-Query option.
|
|
--
|
|
-- @name COAP.header.options.uri_query.build
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param val String representing a query string in the URI.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.uri_query.build = function(val)
|
|
return COAP.header.options.value.string.build(val)
|
|
end
|
|
|
|
--- Parses a CoAP message header Uri-Query option.
|
|
--
|
|
-- @name COAP.header.options.uri_query.parse
|
|
--
|
|
-- 5.10.1. Uri-Host, Uri-Port, Uri-Path, and Uri-Query
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.uri_query.parse = function(buf)
|
|
return COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
--- Builds a CoAP message header Block option.
|
|
--
|
|
-- @name COAP.header.options.block.build
|
|
--
|
|
-- For large payloads that would be too large for the underlying
|
|
-- transport, block transfers exist. This allows endpoints to transfer
|
|
-- payloads in small chunks. This is very common, and is frequently
|
|
-- used when transferring the <code>/.well-known/core</code> resource
|
|
-- due to its size.
|
|
--
|
|
-- As of the writing of this function, the block transfer definition
|
|
-- is a draft undergoing active revision.
|
|
--
|
|
-- https://tools.ietf.org/html/draft-ietf-core-block-19
|
|
--
|
|
-- @see COAP.header.options.block1.build
|
|
-- @see COAP.header.options.block2.build
|
|
--
|
|
-- @param val Table representing the block's parameters.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.value.block.build = function(val)
|
|
assert(type(val) == "table")
|
|
|
|
-- Let the uint parser do the initial encoding, since it can handle
|
|
-- 1-3 byte uints, even though the block number field can only be 4,
|
|
-- 12, or 20 bits. The encoding guarantees that the 4 LSBs can be
|
|
-- used for the remaining two fields.
|
|
--
|
|
-- Note that we have to handle zero as a special case since the uint
|
|
-- will be represented by the absence of any bytes, but we need a
|
|
-- single byte to encode the remaining two fields.
|
|
local num = val.number
|
|
assert(type(num) == "number")
|
|
assert(val.number >= 0)
|
|
assert(val.number <= 1048575)
|
|
|
|
num = num << 1
|
|
|
|
local mf = val.more
|
|
assert(type(mf) == "boolean")
|
|
if mf then
|
|
num = num | 0x1
|
|
end
|
|
|
|
num = num << 3
|
|
|
|
local length = val.length
|
|
assert(type(length) == "number")
|
|
assert(val.length >= 16)
|
|
assert(val.length <= 1024)
|
|
|
|
local map = {[16]=0, [32]=1, [64]=2, [128]=3, [256]=4, [512]=5, [1024]=6}
|
|
local szx = map[length]
|
|
assert(szx)
|
|
|
|
num = num | szx
|
|
|
|
-- The final number that results from combining all the fields
|
|
-- should fit within 3 bytes when built.
|
|
assert(num >= 0)
|
|
assert(num <= 16777215)
|
|
|
|
-- Let the uint builder do the initial encoding, since it can handle
|
|
-- 1-3 byte uints.
|
|
--
|
|
-- There is a special case that if all fields are zero/false, then
|
|
-- no bytes should be contained in the value of the block option.
|
|
-- This is due to the number zero being represented as the absence
|
|
-- of any bytes.
|
|
local str = COAP.header.options.value.uint.build(num)
|
|
|
|
-- Finally, we want to check that we haven't over-shifted, which is
|
|
-- characterized by the result being longer than expected based on
|
|
-- the original number.
|
|
if val.number == 0 and val.more == false and val.length == 16 then
|
|
assert(#str == 0)
|
|
elseif val.number <= 15 then
|
|
assert(#str == 1)
|
|
elseif val.number <= 4095 then
|
|
assert(#str == 2)
|
|
else
|
|
assert(#str == 3)
|
|
end
|
|
|
|
return str
|
|
end
|
|
|
|
--- Parses a CoAP message header Block option.
|
|
--
|
|
-- @name COAP.header.options.block.parse
|
|
--
|
|
-- https://tools.ietf.org/html/draft-ietf-core-block-19
|
|
--
|
|
-- @see COAP.header.options.block1.parse
|
|
-- @see COAP.header.options.block2.parse
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val Table representing the option.
|
|
COAP.header.options.value.block.parse = function(buf)
|
|
assert(#buf >= 0)
|
|
assert(#buf <= 3)
|
|
|
|
-- Let the uint parser do the initial decoding, since it can handle
|
|
-- 1-3 byte uints.
|
|
local num = COAP.header.options.value.uint.parse(buf)
|
|
assert(num >= 0)
|
|
assert(num <= 16777215)
|
|
|
|
-- Extract size exponent which represents 2 to the power of 4 + szx.
|
|
--
|
|
-- Note that this field could have a value as high as 7, it is only
|
|
-- allowed to go up to 6. This prevents the option's value from
|
|
-- being misinterpreted as the payload marker.
|
|
local szx = num & 0x7
|
|
if szx == 7 then
|
|
szx = 6
|
|
end
|
|
|
|
local length = 2 ^ (4 + szx)
|
|
assert(length >= 16)
|
|
assert(length <= 1024)
|
|
|
|
num = num >> 3
|
|
|
|
-- Extract more flag which indicates whether this is the last block.
|
|
local mf = ((num & 0x1) == 0x1)
|
|
assert(type(mf) == "boolean")
|
|
|
|
num = num >> 1
|
|
|
|
-- The remainder of the number is the block number in sequence.
|
|
assert(num >= 0)
|
|
assert(num <= 1048575)
|
|
|
|
return {
|
|
["number"] = num,
|
|
["more"] = mf,
|
|
["length"] = length,
|
|
}
|
|
end
|
|
|
|
--- Builds a CoAP message's Empty header option value.
|
|
--
|
|
-- @name COAP.header.options.value.empty.parse
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param val Parameter is ignored, existing only to keep API
|
|
-- consistent.
|
|
--
|
|
-- @return str Empty string.
|
|
COAP.header.options.value.empty.build = function(val)
|
|
assert(type(val) == "nil")
|
|
return ""
|
|
end
|
|
|
|
--- Parses a CoAP message Empty header option value.
|
|
--
|
|
-- @name COAP.header.options.value.empty.parse
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param buf Parameter is ignored, existing only to keep API
|
|
-- consistent.
|
|
--
|
|
-- @return val Nil due to the option being empty.
|
|
COAP.header.options.value.empty.parse = function(buf)
|
|
assert(type(buf) == "string", ("Expected 'string', got '%s'."):format(type(buf)))
|
|
return nil
|
|
end
|
|
|
|
--- Builds a CoAP message Opaque header option value.
|
|
--
|
|
-- @name COAP.header.options.value.opaque.build
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param str String representing an opaque option value.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.value.opaque.build = function(str)
|
|
assert(type(str) == "string", ("Expected 'string', got '%s'."):format(type(str)))
|
|
return str
|
|
end
|
|
|
|
--- Parses a CoAP message Opaque header option value.
|
|
--
|
|
-- @name COAP.header.options.value.opaque.parse
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.value.opaque.parse = function(buf)
|
|
assert(type(buf) == "string", ("Expected 'string', got '%s'."):format(type(buf)))
|
|
return buf
|
|
end
|
|
|
|
--- Builds a CoAP message String header option value.
|
|
--
|
|
-- @name COAP.header.options.value.string.build
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param str String representing a string option value.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.value.string.build = function(str)
|
|
assert(type(str) == "string", ("Expected 'string', got '%s'."):format(type(str)))
|
|
return str
|
|
end
|
|
|
|
--- Parses a CoAP message String header option value.
|
|
--
|
|
-- @name COAP.header.options.value.string.parse
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val String representing the option's value.
|
|
COAP.header.options.value.string.parse = function(buf)
|
|
assert(type(buf) == "string", ("Expected 'string', got '%s'."):format(type(buf)))
|
|
return buf
|
|
end
|
|
|
|
--- Builds a CoAP message Uint header option value.
|
|
--
|
|
-- @name COAP.header.options.value.uint.build
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param val Number representing a Uint option value.
|
|
--
|
|
-- @return str String representing the option's value.
|
|
COAP.header.options.value.uint.build = function(val)
|
|
assert(type(val) == "number")
|
|
assert(val >= 0)
|
|
assert(val <= 4294967295)
|
|
|
|
if val == 0 then
|
|
return ""
|
|
end
|
|
-- strip leading null bytes to use smallest space
|
|
return string.pack(">I16", val):gsub("^\0*","")
|
|
end
|
|
|
|
--- Parses a CoAP message Uint header option value.
|
|
--
|
|
-- @name COAP.header.options.value.uint.parse
|
|
--
|
|
-- 3.2. Option Value Formats
|
|
--
|
|
-- @param buf String from which to parse the option.
|
|
--
|
|
-- @return val Number representing the option's value.
|
|
COAP.header.options.value.uint.parse = function(buf)
|
|
assert(type(buf) == "string")
|
|
assert(#buf >= 0)
|
|
assert(#buf <= 16)
|
|
|
|
if #buf == 0 then
|
|
return 0
|
|
end
|
|
|
|
local val = string.unpack(">I" .. #buf, buf)
|
|
|
|
-- There should be no way for this to fail.
|
|
assert(val)
|
|
assert(type(val) == "number")
|
|
|
|
return val
|
|
end
|
|
|
|
--- Build the variable-length option delta and length field.
|
|
--
|
|
-- @name COAP.header.options.delta_length.build
|
|
--
|
|
-- Due to the interleaving of these two fields they are handled
|
|
-- together, since they can appear in nine forms, with the first byte
|
|
-- holding a nibble for each:
|
|
-- 1) D|L
|
|
-- 2) D|L D
|
|
-- 3) D|L L
|
|
-- 4) D|L D D
|
|
-- 5) D|L D L
|
|
-- 6) D|L L L
|
|
-- 7) D|L D D L
|
|
-- 8) D|L D L L
|
|
-- 9) D|L D D L L
|
|
--
|
|
-- The 4 bits reserved in the header for the delta and length are
|
|
-- not enough to represent the large numbers required by the
|
|
-- options. For this reason there is a 1 or 2-byte field
|
|
-- conditionally added to the option's header to extend the range
|
|
-- the deltas and lengths can represent.
|
|
--
|
|
-- The delta field can represent:
|
|
-- Low : 0 as 0000
|
|
-- High: 12 as 1100
|
|
--
|
|
-- With one extra delta byte, it can represent:
|
|
-- Low : 13 as 1101 00000000 (13 + 0)
|
|
-- High: 268 as 1101 11111111 (13 + 255)
|
|
--
|
|
-- With two extra delta bytes, it can represent:
|
|
-- Low : 269 as 1110 00000000 00000000 (269 + 0)
|
|
-- High: 65804 as 1110 11111111 11111111 (269 + 65535)
|
|
--
|
|
-- 3.1. Option Format
|
|
--
|
|
-- @param delta Number representing the option ID's delta.
|
|
-- @param length Number representing the length of the option's value.
|
|
--
|
|
-- @return str String representing the delta and length fields.
|
|
COAP.header.options.delta_length.build = function(delta, length)
|
|
local build = function(num)
|
|
assert(type(num) == "number")
|
|
assert(num >= 0)
|
|
assert(num <= 65804)
|
|
|
|
if num <= 12 then
|
|
return num, ""
|
|
end
|
|
|
|
if num <= 268 then
|
|
return 13, string.pack("B", num - 13)
|
|
end
|
|
|
|
return 14, string.pack(">I2", num - 269)
|
|
end
|
|
|
|
local d1, d2 = build(delta)
|
|
local l1, l2 = build(length)
|
|
|
|
d1 = d1 << 4
|
|
|
|
return string.pack("B", d1 | l1) .. d2 .. l2
|
|
end
|
|
|
|
--- Parse the variable-length option delta and length field.
|
|
--
|
|
-- @name COAP.header.options.delta_length.parse
|
|
--
|
|
-- Due to the interleaving of these two fields they are handled
|
|
-- together. See <ref>COAP.header.options.delta_length_build</ref> for details.
|
|
--
|
|
-- 3.1. Option Format
|
|
--
|
|
-- @param buf String from which to parse the fields.
|
|
-- @param pos Position from which to start parsing.
|
|
--
|
|
-- @return pos Position at which parsing stopped on success, or false
|
|
-- on failure.
|
|
-- @return delta Delta value of the option's ID on success, or nil on
|
|
-- failure.
|
|
-- @return length Length of the option's value on success, or nil on
|
|
-- failure.
|
|
-- @return err nil on success, or an error message on failure.
|
|
COAP.header.options.delta_length.parse = function(buf, pos)
|
|
assert(type(buf) == "string")
|
|
if #buf < 1 then
|
|
return false, nil, nil, "Cannot parse a string of less than one byte."
|
|
end
|
|
|
|
if not pos or pos == 0 then
|
|
pos = 1
|
|
end
|
|
assert(type(pos) == "number")
|
|
assert(pos <= #buf)
|
|
|
|
local delta_and_length, pos = string.unpack("B", buf, pos)
|
|
if not pos then
|
|
return false, nil, nil, delta_and_length
|
|
end
|
|
local delta = delta_and_length >> 4
|
|
local length = delta_and_length & 0x0F
|
|
|
|
-- Sanity check the first byte's value.
|
|
if delta == 15 then
|
|
return false, nil, nil, "Delta was 0xF, but a Packet Marker was not expected."
|
|
end
|
|
|
|
if length == 15 then
|
|
return false, nil, nil, "Length was 0xF, but a Packet Marker was not expected."
|
|
end
|
|
|
|
-- Sanity check the length required to parse the remainder of the fields.
|
|
local required_bytes = 0
|
|
local dspec = nil
|
|
local lspec = nil
|
|
|
|
if delta == 13 then
|
|
required_bytes = required_bytes + 1
|
|
dspec = "B"
|
|
elseif delta == 14 then
|
|
required_bytes = required_bytes + 2
|
|
delta = 269
|
|
dspec = ">I2"
|
|
end
|
|
|
|
if length == 13 then
|
|
required_bytes = required_bytes + 1
|
|
lspec = "B"
|
|
elseif length == 14 then
|
|
required_bytes = required_bytes + 2
|
|
length = 269
|
|
lspec = ">I2"
|
|
end
|
|
|
|
if pos + required_bytes - 1 > #buf then
|
|
return false, nil, nil, "Option delta and length fields extend past end of buffer."
|
|
end
|
|
|
|
-- Extract the remaining bytes of each field.
|
|
if dspec then
|
|
local num
|
|
num, pos = string.unpack(dspec, buf, pos)
|
|
if not pos then
|
|
return false, nil, nil, num
|
|
end
|
|
delta = delta + num
|
|
end
|
|
|
|
if lspec then
|
|
local num
|
|
num, pos = string.unpack(lspec, buf, pos)
|
|
if not pos then
|
|
return false, nil, nil, num
|
|
end
|
|
length = length + num
|
|
end
|
|
|
|
return pos, delta, length, nil
|
|
end
|
|
|
|
--- Finds the first instance of an option type in a header.
|
|
--
|
|
-- @name COAP.header.find_option
|
|
--
|
|
-- @see COAP.header.find_options
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param name String naming an option type.
|
|
--
|
|
-- @return opt Table representing option on success, or nil if one was
|
|
-- not found.
|
|
COAP.header.find_option = function(hdr, name)
|
|
assert(type(hdr) == "table")
|
|
assert(type(name) == "string")
|
|
|
|
local opts = COAP.header.find_options(hdr, name, 1)
|
|
if next(opts) == nil then
|
|
return nil
|
|
end
|
|
|
|
return opts[1]
|
|
end
|
|
|
|
--- Finds all instances of an option type in a header.
|
|
--
|
|
-- @name COAP.header.find_options
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param name String naming an option type.
|
|
-- @param max Maximum number of options to return.
|
|
--
|
|
-- @return opts Table containing option all options found, may be
|
|
-- empty.
|
|
COAP.header.find_options = function(hdr, name, max)
|
|
assert(type(hdr) == "table")
|
|
assert(type(name) == "string")
|
|
assert(not max or type(max) == "number")
|
|
|
|
local opts = {}
|
|
|
|
local count = 1
|
|
for _, opt in ipairs(hdr.options) do
|
|
if opt.name == name then
|
|
table.insert(opts, opt.value)
|
|
if max and count >= max then
|
|
break
|
|
end
|
|
count = count + 1
|
|
end
|
|
end
|
|
|
|
return opts
|
|
end
|
|
|
|
COAP.payload.content_formats = {
|
|
["text/plain"] = "text_plain",
|
|
["application/link-format"] = "application_link_format",
|
|
["application/xml"] = "application_xml",
|
|
["application/octet-stream"] = "application_octet_stream",
|
|
["application/exi"] = "application_exi",
|
|
["application/json"] = "application_json",
|
|
}
|
|
|
|
--- Parse the payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.parse
|
|
--
|
|
-- 5.5. Payloads and Representations
|
|
--
|
|
-- Never use this function directly on a payload that has a Block
|
|
-- option, as there will only be a partial payload in such a message.
|
|
-- The top-level <ref>COAP.parse</ref> is smart enough not to
|
|
-- auto-parse messages with partial payloads.
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return val Object containing parsed payload on success, string
|
|
-- containing the error message on failure.
|
|
COAP.payload.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string", type(buf))
|
|
|
|
-- Find the content format option which defines the manner in which
|
|
-- the payload should be interpreted.
|
|
local cf = COAP.header.find_option(hdr, "content_format")
|
|
|
|
-- 5.5.2. Diagnostic Payload
|
|
--
|
|
-- If there's no content-format option, then the payload represents
|
|
-- a human-readable string in UTF-8, for which we already have a
|
|
-- parser.
|
|
if not cf then
|
|
return true, COAP.header.options.value.string.parse(buf)
|
|
end
|
|
|
|
-- If the content format wasn't recognized, it'll come back as a
|
|
-- number and we'll just log that and return the raw payload.
|
|
if type(cf) == "number" then
|
|
stdnse.debug1("Content format ID %d not recognized for payload.", cf)
|
|
return false, buf
|
|
end
|
|
|
|
-- Find the parser associated with the content format.
|
|
local fn_name = COAP.payload.content_formats[cf]
|
|
if not fn_name then
|
|
stdnse.debug1("Content format %s not implemented for payload.", cf)
|
|
return false, buf
|
|
end
|
|
|
|
-- Run the parser associated with the content format.
|
|
local fn = COAP.payload[fn_name].parse
|
|
assert(fn)
|
|
|
|
return fn(hdr, buf)
|
|
end
|
|
|
|
--- Parse the Plain Text payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.text_plain.parse
|
|
--
|
|
-- https://tools.ietf.org/html/rfc2046
|
|
-- https://tools.ietf.org/html/rfc3676
|
|
--
|
|
-- This function will return its input, since plain text is assumed to
|
|
-- have no additional structure.
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return val String containing parsed payload on success, string
|
|
-- containing the error message on failure.
|
|
COAP.payload.text_plain.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string")
|
|
|
|
return true, buf
|
|
end
|
|
|
|
--- Parse the Link Format payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.link_format.parse
|
|
--
|
|
-- https://tools.ietf.org/html/rfc6690
|
|
--
|
|
-- This format is complicated enough that parsing it accurately is
|
|
-- unlikely to be worth the effort. As a result, we have chosen the
|
|
-- following simplifications.
|
|
-- 1) URIs can contain any character except '>'.
|
|
-- 2) Parameters can have two forms:
|
|
-- a) ;name=value-with-semicolons-and-commas-forbidden
|
|
-- b) ;name="value-with-semicolons-and-commas-permitted"
|
|
-- If there is a need for full parsing, it can be addressed later.
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return val Table containing parsed payload on success, string
|
|
-- containing the error message on failure.
|
|
COAP.payload.application_link_format.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string")
|
|
|
|
local P = lpeg.P
|
|
local S = lpeg.S
|
|
local Cg = lpeg.Cg
|
|
local Cs = lpeg.Cs
|
|
local Ct = lpeg.Ct
|
|
|
|
local param_value_quoted = P'"' * Cs((P(1) - P'"')^0) * P'"'
|
|
local param_value_bare = Cs((P(1) - S';,')^0)
|
|
local param_value = param_value_quoted + param_value_bare
|
|
local param_name = Cs((P(1) - P'=')^1)
|
|
local param = Ct(P';' * Cg(param_name, 'name') * P'=' * Cg(param_value, 'value'))
|
|
local uri = P'<' * Cs((P(1) - P'>')^1) * P'>'
|
|
local link = Ct(Cg(uri, 'name') * Cg(Ct(param^0), 'parameters'))
|
|
local patt = Ct(link * (P',' * link)^0)
|
|
|
|
local matches = lpeg.match(patt, buf)
|
|
if not matches then
|
|
return false, ("Failed to format payload.")
|
|
end
|
|
|
|
return true, matches
|
|
end
|
|
|
|
--- Parse the XML payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.application_xml.parse
|
|
--
|
|
-- https://tools.ietf.org/html/rfc3023
|
|
--
|
|
-- This function is unimplemented.
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return response Object containing parsed payload on success,
|
|
-- string containing the error message on failure.
|
|
COAP.payload.application_xml.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string")
|
|
|
|
return false, "Unimplemented"
|
|
end
|
|
|
|
--- Parse the Octet Stream payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.application_octet_stream.parse
|
|
--
|
|
-- https://tools.ietf.org/html/rfc2045
|
|
-- https://tools.ietf.org/html/rfc2046
|
|
--
|
|
-- This function will return its input, since it is assumed to have no
|
|
-- additional structure.
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return val String containing parsed payload on success, string
|
|
-- containing the error message on failure.
|
|
COAP.payload.application_octet_stream.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string")
|
|
|
|
return true, buf
|
|
end
|
|
|
|
--- Parse the EXI payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.exi.parse
|
|
--
|
|
-- https://www.w3.org/TR/2014/REC-exi-20140211/
|
|
--
|
|
-- This function is unimplemented.
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return response Object containing parsed payload on success,
|
|
-- string containing the error message on failure.
|
|
COAP.payload.application_exi.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string")
|
|
|
|
return false, "Unimplemented"
|
|
end
|
|
|
|
--- Parse the JSON payload of a CoAP message.
|
|
--
|
|
-- @name COAP.payload.json.parse
|
|
--
|
|
-- https://tools.ietf.org/html/rfc7159
|
|
--
|
|
-- @param hdr Table representing a message header.
|
|
-- @param buf String from which to parse the payload.
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return response Object containing parsed payload on success,
|
|
-- string containing the error message on failure.
|
|
COAP.payload.application_json.parse = function(hdr, buf)
|
|
assert(type(hdr) == "table")
|
|
assert(type(buf) == "string")
|
|
|
|
return json.parse(buf)
|
|
end
|
|
|
|
Comm = {
|
|
--- Creates a new Client instance.
|
|
--
|
|
-- @name Comm.new
|
|
--
|
|
-- @param host String as received by the action method.
|
|
-- @param port Number as received by the action method.
|
|
-- @param options Table as received by the action method.
|
|
-- @return o Instance of Client.
|
|
new = function(self, host, port, options)
|
|
local o = {host = host, port = port, options = options or {}}
|
|
-- Choose something random, while still giving lots of the 16-bit range
|
|
-- available to grow into.
|
|
o["message_id"] = math.random(16384)
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
--- Connects to the CoAP endpoint.
|
|
--
|
|
-- @name Comm.connect
|
|
--
|
|
-- @return status true on success, false on failure.
|
|
-- @return err string containing the error message on failure.
|
|
connect = function(self, options)
|
|
local pkt = self:build(options)
|
|
local sd, response, _, _ = comm.tryssl(self.host, self.port, pkt, {["proto"] = "udp"})
|
|
if not sd then
|
|
return false, response
|
|
end
|
|
|
|
-- The socket connected successfully over whichever protocol.
|
|
self.socket = sd
|
|
|
|
-- We now have some data that came back from the connection.
|
|
return self:parse(response)
|
|
end,
|
|
|
|
--- Sends a CoAP message.
|
|
--
|
|
-- @name Comm.send
|
|
--
|
|
-- @param pkt String representing a raw message.
|
|
-- @return status true on success, false on failure.
|
|
-- @return err string containing the error message on failure.
|
|
send = function(self, pkt)
|
|
assert(type(pkt) == "string")
|
|
return self.socket:send(pkt)
|
|
end,
|
|
|
|
--- Receives an MQTT control packet.
|
|
--
|
|
-- @name Comm.receive
|
|
--
|
|
-- @return status True on success, false on failure.
|
|
-- @return response String representing a raw message on success,
|
|
-- string containing the error message on failure.
|
|
receive = function(self)
|
|
local status, pkt = self.socket:receive()
|
|
if not status then
|
|
return false, "Failed to receive a response from the server."
|
|
end
|
|
|
|
return true, pkt
|
|
end,
|
|
|
|
--- Builds a CoAP message.
|
|
--
|
|
-- @name Comm.build
|
|
--
|
|
-- @param options Table of options accepted by the requested type of
|
|
-- message.
|
|
-- @return status true on success, false on failure.
|
|
-- @return response String representing a raw message on success, or
|
|
-- containing the error message on failure.
|
|
build = function(self, options, payload)
|
|
assert(type(options) == "table")
|
|
|
|
-- Augment with a message ID we control.
|
|
if not options.id then
|
|
self.message_id = self.message_id + 1
|
|
options.id = self.message_id
|
|
end
|
|
|
|
return COAP.header.build(options, payload)
|
|
end,
|
|
|
|
--- Parses a CoAP message.
|
|
--
|
|
-- @name Comm.parse
|
|
--
|
|
-- @param buf String from which to parse the message.
|
|
-- @param pos Position from which to start parsing.
|
|
-- @return pos String index on success, false on failure.
|
|
-- @return response Table representing a CoAP message on success,
|
|
-- string containing the error message on failure.
|
|
parse = function(self, buf, pos)
|
|
assert(type(buf) == "string")
|
|
|
|
if not pos then
|
|
pos = 0
|
|
end
|
|
assert(type(pos) == "number")
|
|
assert(pos < #buf)
|
|
|
|
local pos, hdr = COAP.parse(buf, pos)
|
|
if not pos then
|
|
return false, hdr
|
|
end
|
|
|
|
return pos, hdr
|
|
end,
|
|
|
|
--- Disconnects from the CoAP endpoint.
|
|
--
|
|
-- @name Comm.close
|
|
close = function(self)
|
|
return self.socket:close()
|
|
end,
|
|
}
|
|
|
|
Helper = {
|
|
--- Creates a new Helper instance.
|
|
--
|
|
-- @name Helper.create
|
|
--
|
|
-- @param host String as received by the action method.
|
|
-- @param port Number as received by the action method.
|
|
-- @param options Table as received by the action method.
|
|
-- @return o instance of Client
|
|
new = function(self, host, port, opt)
|
|
local o = { host = host, port = port, opt = opt or {} }
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end,
|
|
|
|
--- Connects to the CoAP endpoint.
|
|
--
|
|
-- @name Helper.connect
|
|
--
|
|
-- @param options Table of options for the initial message.
|
|
-- @return status True on success, false on failure.
|
|
-- @return response Table representing the response on success,
|
|
-- string containing the error message on failure.
|
|
connect = function(self, options)
|
|
if not options.code then
|
|
options.code = "get"
|
|
end
|
|
|
|
if not options.type then
|
|
options.type = "confirmable"
|
|
end
|
|
|
|
if not options.options then
|
|
options.options = {}
|
|
end
|
|
|
|
assert(options.uri)
|
|
local components = stringaux.strsplit("/", options.uri)
|
|
for _, component in ipairs(components) do
|
|
if component ~= "" then
|
|
table.insert(options.options, {["name"] = "uri_path", ["value"] = component})
|
|
end
|
|
end
|
|
|
|
self.comm = Comm:new(self.host, self.port, self.opt)
|
|
|
|
local status, response = self.comm:connect(options)
|
|
if not status then
|
|
return false, response
|
|
end
|
|
|
|
-- If the response's ID is not what we expect, then we're going to assume
|
|
-- that we're not talking to a CoAP service.
|
|
if response.id ~= self.comm.message_id then
|
|
return false, "Message ID in response does not match request."
|
|
end
|
|
|
|
return status, response
|
|
end,
|
|
|
|
--- Sends a request to the CoAP endpoint.
|
|
--
|
|
-- @name Helper.send
|
|
--
|
|
-- @param options Table of options for the message.
|
|
-- @param payload Payload of message.
|
|
-- @return status True on success, false on failure.
|
|
-- @return err String containing the error message on failure.
|
|
send = function(self, options, payload)
|
|
assert(type(options) == "table")
|
|
|
|
local pkt = self.comm:build(options, payload)
|
|
|
|
return self.comm:send(pkt)
|
|
end,
|
|
|
|
--- Sends a request to the CoAP, and receive a response.
|
|
--
|
|
-- @name Helper.request
|
|
--
|
|
-- @param options Table of options for the message.
|
|
-- @param payload String containing the message body.
|
|
-- @return status True on success, false on failure.
|
|
-- @return response Table representing a message with the
|
|
-- corresponding message ID on success, string containing
|
|
-- the error message on failure.
|
|
request = function(self, options, payload)
|
|
assert(type(options) == "table")
|
|
|
|
local status, err = self:send(options, payload)
|
|
if not status then
|
|
return false, err
|
|
end
|
|
|
|
local id
|
|
if options.id then
|
|
id = options.id
|
|
else
|
|
id = self.comm.o["message_id"]
|
|
end
|
|
|
|
return self:receive({id})
|
|
end,
|
|
|
|
--- Listens for a response matching a list of types.
|
|
--
|
|
-- @name Helper.receive
|
|
--
|
|
-- @param ids Table of message IDs to wait for.
|
|
-- @param timeout Number of seconds to listen for matching response,
|
|
-- defaults to 5s.
|
|
-- @return status True on success, false on failure.
|
|
-- @return response Table representing any message on success,
|
|
-- string containing the error message on failure.
|
|
receive = function(self, ids, timeout)
|
|
assert(type(ids) == "table")
|
|
|
|
if not timeout then
|
|
timeout = 5
|
|
end
|
|
assert(type(timeout) == "number")
|
|
|
|
local end_time = nmap.clock_ms() + timeout * 1000
|
|
while true do
|
|
-- Get the raw packet from the socket.
|
|
local status, pkt = self.comm:receive()
|
|
if not status then
|
|
return false, pkt
|
|
end
|
|
|
|
-- Parse the raw packet into a table.
|
|
local status, hdr = self.comm:parse(pkt)
|
|
if not status then
|
|
return false, hdr
|
|
end
|
|
|
|
-- Check for messages matching our message IDs.
|
|
for _, id in pairs(ids) do
|
|
if hdr.id == id then
|
|
return true, hdr
|
|
end
|
|
end
|
|
|
|
-- Check timeout, but only if we care about it.
|
|
if timeout > 0 then
|
|
if nmap.clock_ms() >= end_time then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return false, ("No messages received in %d seconds matching desired message IDs."):format(timeout)
|
|
end,
|
|
|
|
-- Closes the socket with the endpoint.
|
|
--
|
|
-- @name Helper.close
|
|
close = function(self)
|
|
end,
|
|
}
|
|
|
|
-- Skip unit tests unless we're explicitly testing.
|
|
if not unittest.testing() then
|
|
return _ENV
|
|
end
|
|
|
|
local _test_id = 0
|
|
local function test_id()
|
|
_test_id = _test_id + 1
|
|
return _test_id
|
|
end
|
|
|
|
test_suite = unittest.TestSuite:new()
|
|
|
|
for test_name, test_code in pairs(COAP.header.codes.ids) do
|
|
local test_cls = test_code[1]
|
|
local test_dtl = test_code[2]
|
|
|
|
-- Build the packet.
|
|
local str = COAP.header.codes.build(test_name)
|
|
|
|
-- Parse, implicitly from the first character.
|
|
local pos, name = COAP.header.codes.parse(str)
|
|
test_suite:add_test(unittest.equal(name, test_name), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the zero-indexed first character.
|
|
local pos, name = COAP.header.codes.parse(str, 0)
|
|
test_suite:add_test(unittest.equal(name, test_name), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed first character.
|
|
local pos, name = COAP.header.codes.parse(str, 1)
|
|
test_suite:add_test(unittest.equal(name, test_name), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed second character.
|
|
local pos, name = COAP.header.codes.parse("!" .. str, 2)
|
|
test_suite:add_test(unittest.equal(name, test_name), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #str + 2), test_id())
|
|
end
|
|
|
|
local tests = {
|
|
{ 0, string.char( )},
|
|
{ 1, string.char(0x01 )},
|
|
{ 2, string.char(0x02 )},
|
|
{ 254, string.char(0xFE )},
|
|
{ 255, string.char(0xFF )},
|
|
{ 256, string.char(0x01, 0x00 )},
|
|
{ 257, string.char(0x01, 0x01 )},
|
|
{ 65534, string.char(0xFF, 0xFE )},
|
|
{ 65535, string.char(0xFF, 0xFF )},
|
|
{ 65536, string.char(0x01, 0x00, 0x00 )},
|
|
{ 65537, string.char(0x01, 0x00, 0x01 )},
|
|
{ 16777214, string.char(0xFF, 0xFF, 0xFE )},
|
|
{ 16777215, string.char(0xFF, 0xFF, 0xFF )},
|
|
{ 16777216, string.char(0x01, 0x00, 0x00, 0x00)},
|
|
{ 16777217, string.char(0x01, 0x00, 0x00, 0x01)},
|
|
{4294967293, string.char(0xFF, 0xFF, 0xFF, 0xFD)},
|
|
{4294967294, string.char(0xFF, 0xFF, 0xFF, 0xFE)},
|
|
{4294967295, string.char(0xFF, 0xFF, 0xFF, 0xFF)},
|
|
}
|
|
|
|
for _, test in ipairs(tests) do
|
|
local test_num = test[1]
|
|
local test_str = test[2]
|
|
|
|
-- Build the field.
|
|
local str = COAP.header.options.value.uint.build(test_num)
|
|
test_suite:add_test(unittest.equal(str, test_str), test_id())
|
|
|
|
-- Parse the field.
|
|
local num = COAP.header.options.value.uint.parse(test_str)
|
|
test_suite:add_test(unittest.equal(num, test_num), test_id())
|
|
end
|
|
|
|
-- 3.1. Option Format
|
|
-- There are five different values at which to test the options
|
|
-- delta and length fields:
|
|
-- 1) Start
|
|
-- 2) Start + 1
|
|
-- 3) Middle
|
|
-- 4) End - 1
|
|
-- 5) End
|
|
-- This should be done for each of the three possible field lengths,
|
|
-- and at a variety of locations in the buffer.
|
|
local tests = {
|
|
{ 0, 0, string.char(0x00 )},
|
|
{ 1, 0, string.char(0x10 )},
|
|
{ 0, 1, string.char(0x01 )},
|
|
{ 1, 1, string.char(0x11 )},
|
|
{ 2, 1, string.char(0x21 )},
|
|
{ 1, 2, string.char(0x12 )},
|
|
{ 2, 2, string.char(0x22 )},
|
|
{ 11, 11, string.char(0xBB )},
|
|
{ 12, 11, string.char(0xCB )},
|
|
{ 11, 12, string.char(0xBC )},
|
|
{ 12, 12, string.char(0xCC )},
|
|
{ 13, 12, string.char(0xDC, 0x00 )},
|
|
{ 12, 13, string.char(0xCD, 0x00 )},
|
|
{ 13, 13, string.char(0xDD, 0x00, 0x00 )},
|
|
{ 14, 13, string.char(0xDD, 0x01, 0x00 )},
|
|
{ 13, 14, string.char(0xDD, 0x00, 0x01 )},
|
|
{ 14, 14, string.char(0xDD, 0x01, 0x01 )},
|
|
{ 267, 267, string.char(0xDD, 0xFE, 0xFE )},
|
|
{ 268, 267, string.char(0xDD, 0xFF, 0xFE )},
|
|
{ 267, 268, string.char(0xDD, 0xFE, 0xFF )},
|
|
{ 268, 268, string.char(0xDD, 0xFF, 0xFF )},
|
|
{ 269, 268, string.char(0xED, 0x00, 0x00, 0xFF )},
|
|
{ 268, 269, string.char(0xDE, 0xFF, 0x00, 0x00 )},
|
|
{ 269, 269, string.char(0xEE, 0x00, 0x00, 0x00, 0x00)},
|
|
{ 270, 269, string.char(0xEE, 0x00, 0x01, 0x00, 0x00)},
|
|
{ 269, 270, string.char(0xEE, 0x00, 0x00, 0x00, 0x01)},
|
|
{ 270, 270, string.char(0xEE, 0x00, 0x01, 0x00, 0x01)},
|
|
{65802, 65802, string.char(0xEE, 0xFF, 0xFD, 0xFF, 0xFD)},
|
|
{65803, 65802, string.char(0xEE, 0xFF, 0xFE, 0xFF, 0xFD)},
|
|
{65802, 65803, string.char(0xEE, 0xFF, 0xFD, 0xFF, 0xFE)},
|
|
{65803, 65803, string.char(0xEE, 0xFF, 0xFE, 0xFF, 0xFE)},
|
|
{65804, 65803, string.char(0xEE, 0xFF, 0xFF, 0xFF, 0xFE)},
|
|
{65803, 65804, string.char(0xEE, 0xFF, 0xFE, 0xFF, 0xFF)},
|
|
{65804, 65804, string.char(0xEE, 0xFF, 0xFF, 0xFF, 0xFF)},
|
|
}
|
|
|
|
for _, test in ipairs(tests) do
|
|
local test_del = test[1]
|
|
local test_len = test[2]
|
|
local test_str = test[3]
|
|
|
|
-- Build the field.
|
|
local str = COAP.header.options.delta_length.build(test_del, test_len)
|
|
test_suite:add_test(unittest.equal(str, test_str), test_id())
|
|
|
|
-- Parse, implicitly from the first character.
|
|
local pos, del, len, err = COAP.header.options.delta_length.parse(test_str)
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
test_suite:add_test(unittest.equal(del, test_del), test_id())
|
|
test_suite:add_test(unittest.equal(len, test_len), test_id())
|
|
test_suite:add_test(unittest.is_nil(err), test_id())
|
|
|
|
-- -- Parse, explicitly from the zero-indexed first character.
|
|
local pos, del, len, err = COAP.header.options.delta_length.parse(test_str, 0)
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
test_suite:add_test(unittest.equal(del, test_del), test_id())
|
|
test_suite:add_test(unittest.equal(len, test_len), test_id())
|
|
test_suite:add_test(unittest.is_nil(err), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed first character.
|
|
local pos, del, len, err = COAP.header.options.delta_length.parse(test_str, 1)
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
test_suite:add_test(unittest.equal(del, test_del), test_id())
|
|
test_suite:add_test(unittest.equal(len, test_len), test_id())
|
|
test_suite:add_test(unittest.is_nil(err), test_id())
|
|
|
|
-- -- Parse, explicitly from the one-indexed second character.
|
|
local pos, del, len, err = COAP.header.options.delta_length.parse("!" .. test_str, 2)
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 2), test_id())
|
|
test_suite:add_test(unittest.equal(del, test_del), test_id())
|
|
test_suite:add_test(unittest.equal(len, test_len), test_id())
|
|
test_suite:add_test(unittest.is_nil(err), test_id())
|
|
|
|
-- Truncate string and attempt to parse, expecting error.
|
|
local short_str = test_str:sub(1, #test_str - 1)
|
|
test_suite:add_test(unittest.equal(#short_str, #test_str - 1), test_id())
|
|
local pos, del, len, err = COAP.header.options.delta_length.parse(short_str)
|
|
test_suite:add_test(unittest.is_false(pos), test_id())
|
|
test_suite:add_test(unittest.is_nil(del), test_id())
|
|
test_suite:add_test(unittest.is_nil(len), test_id())
|
|
test_suite:add_test(unittest.not_nil(err), test_id())
|
|
end
|
|
|
|
-- See section "3.1. Option Format" of the standard.
|
|
local tests = {
|
|
{
|
|
-- Before
|
|
{
|
|
{["name"] = "if_none_match"},
|
|
},
|
|
-- After
|
|
{
|
|
{["name"] = "if_none_match"},
|
|
},
|
|
string.char(0x50)
|
|
},
|
|
{
|
|
-- Before
|
|
{
|
|
{["name"] = "etag", ["value"] = "ETAGETAG"},
|
|
},
|
|
-- After
|
|
{
|
|
{["name"] = "etag", ["value"] = "ETAGETAG"},
|
|
},
|
|
"\x48ETAGETAG"
|
|
},
|
|
{
|
|
-- Before
|
|
{
|
|
{["name"] = "max_age", ["value"] = 0},
|
|
},
|
|
-- After
|
|
{
|
|
{["name"] = "max_age", ["value"] = 0},
|
|
},
|
|
string.char(0xD0, 0x01)
|
|
},
|
|
{
|
|
-- Before
|
|
{
|
|
{["name"] = "max_age", ["value"] = 0},
|
|
{["name"] = "uri_path", ["value"] = "foo"},
|
|
},
|
|
-- After
|
|
{
|
|
{["name"] = "uri_path", ["value"] = "foo"},
|
|
{["name"] = "max_age", ["value"] = 0},
|
|
},
|
|
"\xB3foo\x30"
|
|
},
|
|
{
|
|
-- Before
|
|
{
|
|
{["name"] = "uri_path", ["value"] = ".well-known"},
|
|
{["name"] = "uri_path", ["value"] = "core"},
|
|
},
|
|
-- After
|
|
{
|
|
{["name"] = "uri_path", ["value"] = ".well-known"},
|
|
{["name"] = "uri_path", ["value"] = "core"},
|
|
},
|
|
"\xBB.well-known\x04core"
|
|
},
|
|
{
|
|
-- Before
|
|
{
|
|
{["name"] = "uri_path", ["value"] = ".well-known"},
|
|
{["name"] = "if_none_match"},
|
|
{["name"] = "max_age", ["value"] = 0},
|
|
{["name"] = "etag", ["value"] = "ETAGETAG"},
|
|
{["name"] = "uri_path", ["value"] = "core"},
|
|
},
|
|
-- After
|
|
{
|
|
{["name"] = "etag", ["value"] = "ETAGETAG"},
|
|
{["name"] = "if_none_match"},
|
|
{["name"] = "uri_path", ["value"] = ".well-known"},
|
|
{["name"] = "uri_path", ["value"] = "core"},
|
|
{["name"] = "max_age", ["value"] = 0},
|
|
},
|
|
"\x48ETAGETAG\x10\x6B.well-known\x04core\x30"
|
|
},
|
|
}
|
|
|
|
for _, test in ipairs(tests) do
|
|
local test_opt1 = test[1]
|
|
local test_opt2 = test[2]
|
|
local test_str = test[3]
|
|
|
|
-- Build the packet.
|
|
local str = COAP.header.options.build(test_opt1)
|
|
test_suite:add_test(unittest.equal(str, test_str), test_id())
|
|
|
|
-- Parse, implicitly from the first character.
|
|
local pos, opt = COAP.header.options.parse(test_str)
|
|
test_suite:add_test(unittest.identical(opt, test_opt2), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the zero-indexed first character.
|
|
local pos, opt = COAP.header.options.parse(test_str, 0)
|
|
test_suite:add_test(unittest.identical(opt, test_opt2), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed first character.
|
|
local pos, opt = COAP.header.options.parse(test_str, 1)
|
|
test_suite:add_test(unittest.identical(opt, test_opt2), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed second character.
|
|
local pos, opt = COAP.header.options.parse("!" .. test_str, 2)
|
|
test_suite:add_test(unittest.identical(opt, test_opt2), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 2), test_id())
|
|
end
|
|
|
|
local tests = {
|
|
{
|
|
{
|
|
["version"] = 1,
|
|
["code"] = "get",
|
|
["id"] = 0x1234,
|
|
["type"] = "confirmable",
|
|
["token"] = "nmapcoap",
|
|
["token_length"] = 8,
|
|
["options"] = {
|
|
{["name"] = "uri_path", ["value"] = ".well-known"},
|
|
{["name"] = "uri_path", ["value"] = "core"},
|
|
},
|
|
},
|
|
"\x48\x01\x12\x34nmapcoap\xBB.well-known\x04core"
|
|
},
|
|
}
|
|
|
|
for _, test in ipairs(tests) do
|
|
local test_hdr = test[1]
|
|
local test_str = test[2]
|
|
|
|
-- Build the packet.
|
|
local str = COAP.header.build(test_hdr)
|
|
test_suite:add_test(unittest.equal(str, test_str), test_id())
|
|
|
|
-- Parse, implicitly from the first character.
|
|
local pos, hdr = COAP.header.parse(test_str)
|
|
test_suite:add_test(unittest.identical(hdr, test_hdr), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the zero-indexed first character.
|
|
local pos, hdr = COAP.header.parse(test_str, 0)
|
|
test_suite:add_test(unittest.identical(hdr, test_hdr), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed first character.
|
|
local pos, hdr = COAP.header.parse(test_str, 1)
|
|
test_suite:add_test(unittest.identical(hdr, test_hdr), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 1), test_id())
|
|
|
|
-- Parse, explicitly from the one-indexed second character.
|
|
local pos, hdr = COAP.header.parse("!" .. test_str, 2)
|
|
test_suite:add_test(unittest.identical(hdr, test_hdr), test_id())
|
|
test_suite:add_test(unittest.equal(pos, #test_str + 2), test_id())
|
|
end
|
|
|
|
local tests = {
|
|
{
|
|
"application/link-format",
|
|
"",
|
|
"Failed to format payload."
|
|
},
|
|
{
|
|
"application/link-format",
|
|
"<>",
|
|
"Failed to format payload."
|
|
},
|
|
{
|
|
"application/link-format",
|
|
"<>>",
|
|
"Failed to format payload."
|
|
},
|
|
{
|
|
"application/link-format",
|
|
"<<>",
|
|
{{["name"] = "<", ["parameters"] = {}}}
|
|
},
|
|
{
|
|
"application/link-format",
|
|
"<a>,<b>",
|
|
{
|
|
{["name"] = "a", ["parameters"] = {}},
|
|
{["name"] = "b", ["parameters"] = {}},
|
|
}
|
|
},
|
|
{
|
|
"application/link-format",
|
|
"<a>,<b>;param1=B1",
|
|
{
|
|
{["name"] = "a", ["parameters"] = {}},
|
|
{
|
|
["name"] = "b",
|
|
["parameters"] = {
|
|
{["name"] = "param1", ["value"] = 'B1'}
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"application/link-format",
|
|
"<a>,<b>;param1=B1,<c>;param2=C1;param3=C2",
|
|
{
|
|
{["name"] = "a", ["parameters"] = {}},
|
|
{
|
|
["name"] = "b",
|
|
["parameters"] = {
|
|
{["name"] = "param1", ["value"] = 'B1'}
|
|
}
|
|
},
|
|
{
|
|
["name"] = "c",
|
|
["parameters"] = {
|
|
{["name"] = "param2", ["value"] = 'C1'},
|
|
{["name"] = "param3", ["value"] = 'C2'}
|
|
}
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"application/link-format",
|
|
'<a>,<b>;param1=B1,<c>;param2=C1;param3=C2,<d>;param4=";";param5=",";param6= ",<e>',
|
|
{
|
|
{["name"] = "a", ["parameters"] = {}},
|
|
{
|
|
["name"] = "b",
|
|
["parameters"] = {
|
|
{["name"] = "param1", ["value"] = 'B1'}
|
|
}
|
|
},
|
|
{
|
|
["name"] = "c",
|
|
["parameters"] = {
|
|
{["name"] = "param2", ["value"] = 'C1'},
|
|
{["name"] = "param3", ["value"] = 'C2'}
|
|
}
|
|
},
|
|
{
|
|
["name"] = "d",
|
|
["parameters"] = {
|
|
{["name"] = "param4", ["value"] = ';'},
|
|
{["name"] = "param5", ["value"] = ','},
|
|
{["name"] = "param6", ["value"] = ' "'},
|
|
}
|
|
},
|
|
{["name"] = "e", ["parameters"] = {}},
|
|
}
|
|
},
|
|
{
|
|
"application/json",
|
|
'{}',
|
|
{}
|
|
},
|
|
{
|
|
"application/json",
|
|
'{"a": false}',
|
|
{["a"] = false}
|
|
},
|
|
{
|
|
"application/json",
|
|
'{"a": {"b": true}}',
|
|
{["a"] = {["b"] = true}}
|
|
},
|
|
{
|
|
"text/plain",
|
|
"nmap",
|
|
"nmap"
|
|
},
|
|
{
|
|
"application/octet-stream",
|
|
string.char(0x01, 0x23, 0x45, 0x56, 0x89, 0xAB, 0xCD, 0xEF),
|
|
string.char(0x01, 0x23, 0x45, 0x56, 0x89, 0xAB, 0xCD, 0xEF),
|
|
},
|
|
}
|
|
|
|
for _, test in ipairs(tests) do
|
|
local test_fmt = test[1]
|
|
local test_str = test[2]
|
|
local test_res = test[3]
|
|
|
|
local hdr = {
|
|
["options"] = {
|
|
{
|
|
["name"] = "content_format",
|
|
["value"] = test_fmt
|
|
}
|
|
}
|
|
}
|
|
|
|
-- Parse, implicitly from the first character.
|
|
local status, res = COAP.payload.parse(hdr, test_str)
|
|
test_suite:add_test(unittest.identical(res, test_res), test_id())
|
|
end
|
|
|
|
return _ENV;
|