summaryrefslogtreecommitdiffstats
path: root/nselib/coap.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/coap.lua')
-rw-r--r--nselib/coap.lua2675
1 files changed, 2675 insertions, 0 deletions
diff --git a/nselib/coap.lua b/nselib/coap.lua
new file mode 100644
index 0000000..978146f
--- /dev/null
+++ b/nselib/coap.lua
@@ -0,0 +1,2675 @@
+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;