From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/coap.lua | 2675 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2675 insertions(+) create mode 100644 nselib/coap.lua (limited to 'nselib/coap.lua') 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 " +-- @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 /.well-known/core 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 COAP.header.options.delta_length_build 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 COAP.parse 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", + ",", + { + {["name"] = "a", ["parameters"] = {}}, + {["name"] = "b", ["parameters"] = {}}, + } + }, + { + "application/link-format", + ",;param1=B1", + { + {["name"] = "a", ["parameters"] = {}}, + { + ["name"] = "b", + ["parameters"] = { + {["name"] = "param1", ["value"] = 'B1'} + } + }, + } + }, + { + "application/link-format", + ",;param1=B1,;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", + ',;param1=B1,;param2=C1;param3=C2,;param4=";";param5=",";param6= ",', + { + {["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; -- cgit v1.2.3