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;