181 lines
5.0 KiB
Lua
181 lines
5.0 KiB
Lua
--- Stream class.
|
|
-- Decodes AWS response-stream types, currently application/vnd.amazon.eventstream
|
|
-- @classmod Stream
|
|
|
|
local buf = require("string.buffer")
|
|
local to_hex = require("resty.string").to_hex
|
|
|
|
local Stream = {}
|
|
Stream.__index = Stream
|
|
|
|
|
|
local _HEADER_EXTRACTORS = {
|
|
-- bool true
|
|
[0] = function(stream)
|
|
return true, 0
|
|
end,
|
|
|
|
-- bool false
|
|
[1] = function(stream)
|
|
return false, 0
|
|
end,
|
|
|
|
-- string type
|
|
[7] = function(stream)
|
|
local header_value_len = stream:next_int(16)
|
|
return stream:next_utf_8(header_value_len), header_value_len + 2 -- add the 2 bits read for the length
|
|
end,
|
|
|
|
-- TODO ADD THE REST OF THE DATA TYPES
|
|
-- EVEN THOUGH THEY'RE NOT REALLY USED
|
|
}
|
|
|
|
--- Constructor.
|
|
-- @function aws:Stream
|
|
-- @param chunk string complete AWS response stream chunk for decoding
|
|
-- @param is_hex boolean specify if the chunk bytes are already decoded to hex
|
|
-- @usage
|
|
-- local stream_parser = stream:new("00000120af0310f.......", true)
|
|
-- local next, err = stream_parser:next_message()
|
|
function Stream:new(chunk, is_hex)
|
|
local self = {} -- override 'self' to be the new object/class
|
|
setmetatable(self, Stream)
|
|
|
|
if #chunk < ((is_hex and 32) or 16) then
|
|
return nil, "cannot parse a chunk less than 16 bytes long"
|
|
end
|
|
|
|
self.read_count = 0
|
|
self.chunk = buf.new()
|
|
self.chunk:put((is_hex and chunk) or to_hex(chunk))
|
|
|
|
return self
|
|
end
|
|
|
|
|
|
--- return the next `count` ascii bytes from the front of the chunk
|
|
--- and then trims the chunk of those bytes
|
|
-- @param count number whole utf-8 bytes to return
|
|
-- @return string resulting utf-8 string
|
|
function Stream:next_utf_8(count)
|
|
local utf_bytes = self:next_bytes(count)
|
|
|
|
local ascii_string = ""
|
|
for i = 1, #utf_bytes, 2 do
|
|
local hex_byte = utf_bytes:sub(i, i + 1)
|
|
local ascii_byte = string.char(tonumber(hex_byte, 16))
|
|
ascii_string = ascii_string .. ascii_byte
|
|
end
|
|
return ascii_string
|
|
end
|
|
|
|
--- returns the next `count` bytes from the front of the chunk
|
|
--- and then trims the chunk of those bytes
|
|
-- @param count number whole integer of bytes to return
|
|
-- @return string hex-encoded next `count` bytes
|
|
function Stream:next_bytes(count)
|
|
if not self.chunk then
|
|
return nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)"
|
|
end
|
|
|
|
local bytes = self.chunk:get(count * 2)
|
|
self.read_count = (count) + self.read_count
|
|
|
|
return bytes
|
|
end
|
|
|
|
--- returns the next unsigned int from the front of the chunk
|
|
--- and then trims the chunk of those bytes
|
|
-- @param size integer bit length (8, 16, 32, etc)
|
|
-- @return number whole integer of size specified
|
|
-- @return string the original bytes, for reference/checksums
|
|
function Stream:next_int(size)
|
|
if not self.chunk then
|
|
return nil, nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)"
|
|
end
|
|
|
|
if size < 8 then
|
|
return nil, nil, "cannot work on integers smaller than 8 bits long"
|
|
end
|
|
|
|
local int, err = self:next_bytes(size / 8)
|
|
if err then
|
|
return nil, nil, err
|
|
end
|
|
|
|
return tonumber(int, 16), int
|
|
end
|
|
|
|
--- returns the next message in the chunk, as a table.
|
|
--- can be used as an iterator.
|
|
-- @return table formatted next message from the given constructor chunk
|
|
function Stream:next_message()
|
|
if not self.chunk then
|
|
return nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)"
|
|
end
|
|
|
|
if #self.chunk < 1 then
|
|
return false
|
|
end
|
|
|
|
-- get the message length and pull that many bytes
|
|
--
|
|
-- this is a chicken and egg problem, because we need to
|
|
-- read the message to get the length, to then re-read the
|
|
-- whole message at correct offset
|
|
local msg_len, _, err = self:next_int(32)
|
|
if err then
|
|
return err
|
|
end
|
|
|
|
-- get the headers length
|
|
local headers_len, _, err = self:next_int(32)
|
|
if err then
|
|
return err
|
|
end
|
|
|
|
-- get the preamble checksum
|
|
-- skip it because we're not using UDP
|
|
self:next_int(32)
|
|
|
|
-- pull the headers from the buf
|
|
local headers = {}
|
|
local headers_bytes_read = 0
|
|
|
|
while headers_bytes_read < headers_len do
|
|
-- the next 8-bit int is the "header key length"
|
|
local header_key_len = self:next_int(8)
|
|
local header_key = self:next_utf_8(header_key_len)
|
|
headers_bytes_read = 1 + header_key_len + headers_bytes_read
|
|
|
|
-- next 8-bits is the header type, which is an enum
|
|
local header_type = self:next_int(8)
|
|
headers_bytes_read = 1 + headers_bytes_read
|
|
|
|
-- depending on the header type, depends on how long the header should max out at
|
|
local header_value, header_value_len = _HEADER_EXTRACTORS[header_type](self)
|
|
headers_bytes_read = header_value_len + headers_bytes_read
|
|
|
|
headers[header_key] = header_value
|
|
end
|
|
|
|
-- finally, extract the body as a string by
|
|
-- subtracting what's read so far from the
|
|
-- total length obtained right at the start
|
|
local body = self:next_utf_8(msg_len - self.read_count - 4)
|
|
|
|
-- last 4 bytes is a body checksum
|
|
-- skip it because we're not using UDP
|
|
self:next_int(32)
|
|
|
|
|
|
-- rewind the tape
|
|
self.read_count = 0
|
|
|
|
return {
|
|
headers = headers,
|
|
body = body,
|
|
}
|
|
end
|
|
|
|
return Stream |