2676 lines
75 KiB
Lua
Raw Normal View History

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