diff --git a/dev/h2/h2-tracer.lua b/dev/h2/h2-tracer.lua new file mode 100644 index 000000000..0d06a4b45 --- /dev/null +++ b/dev/h2/h2-tracer.lua @@ -0,0 +1,247 @@ +-- This is an HTTP/2 tracer for a TCP proxy. It will decode the frames that are +-- exchanged between the client and the server and indicate their direction, +-- types, flags and lengths. Lines are prefixed with a connection number modulo +-- 4096 that allows to sort out multiplexed exchanges. In order to use this, +-- simply load this file in the global section and use it from a TCP proxy: +-- +-- global +-- lua-load "dev/h2/h2-tracer.lua" +-- +-- listen h2_sniffer +-- mode tcp +-- bind :8002 +-- filter lua.h2-tracer #hex +-- server s1 127.0.0.1:8003 +-- + +-- define the decoder's class here +Dec = {} +Dec.id = "Lua H2 tracer" +Dec.flags = 0 +Dec.__index = Dec +Dec.args = {} -- args passed by the filter's declaration +Dec.cid = 0 -- next connection ID + +-- prefix to indent responses +res_pfx = " | " + +-- H2 frame types +h2ft = { + [0] = "DATA", + [1] = "HEADERS", + [2] = "PRIORITY", + [3] = "RST_STREAM", + [4] = "SETTINGS", + [5] = "PUSH_PROMISE", + [6] = "PING", + [7] = "GOAWAY", + [8] = "WINDOW_UPDATE", + [9] = "CONTINUATION", +} + +h2ff = { + [0] = { [0] = "ES", [3] = "PADDED" }, -- data + [1] = { [0] = "ES", [2] = "EH", [3] = "PADDED", [5] = "PRIORITY" }, -- headers + [2] = { }, -- priority + [3] = { }, -- rst_stream + [4] = { [0] = "ACK" }, -- settings + [5] = { [2] = "EH", [3] = "PADDED" }, -- push_promise + [6] = { [0] = "ACK" }, -- ping + [7] = { }, -- goaway + [8] = { }, -- window_update + [9] = { [3] = "PADDED" }, -- continuation +} + +function Dec:new() + local dec = {} + + setmetatable(dec, Dec) + dec.do_hex = false + if (Dec.args[1] == "hex") then + dec.do_hex = true + end + + Dec.cid = Dec.cid+1 + -- mix the thread number when multithreading. + dec.cid = Dec.cid + 64 * core.thread + + -- state per dir. [1]=req [2]=res + dec.st = { + [1] = { + hdr = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + fofs = 0, + flen = 0, + ftyp = 0, + fflg = 0, + sid = 0, + tot = 0, + }, + [2] = { + hdr = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + fofs = 0, + flen = 0, + ftyp = 0, + fflg = 0, + sid = 0, + tot = 0, + }, + } + return dec +end + +function Dec:start_analyze(txn, chn) + if chn:is_resp() then + io.write(string.format("[%03x] ", self.cid % 4096) .. res_pfx .. "### res start\n") + else + io.write(string.format("[%03x] ", self.cid % 4096) .. "### req start\n") + end + filter.register_data_filter(self, chn) +end + +function Dec:end_analyze(txn, chn) + if chn:is_resp() then + io.write(string.format("[%03x] ", self.cid % 4096) .. res_pfx .. "### res end: " .. self.st[2].tot .. " bytes total\n") + else + io.write(string.format("[%03x] ", self.cid % 4096) .. "### req end: " ..self.st[1].tot.. " bytes total\n") + end +end + +function Dec:tcp_payload(txn, chn) + local data = { } + local dofs = 1 + local pfx = "" + local dir = 1 + local sofs = 0 + local ft = "" + local ff = "" + + if chn:is_resp() then + pfx = res_pfx + dir = 2 + end + + pfx = string.format("[%03x] ", self.cid % 4096) .. pfx + + -- stream offset before processing + sofs = self.st[dir].tot + + if (chn:input() > 0) then + data = chn:data() + self.st[dir].tot = self.st[dir].tot + chn:input() + end + + if (chn:input() > 0 and self.do_hex ~= false) then + io.write("\n" .. pfx .. "Hex:\n") + for i = 1, #data do + if ((i & 7) == 1) then io.write(pfx) end + io.write(string.format("0x%02x ", data:sub(i, i):byte())) + if ((i & 7) == 0 or i == #data) then io.write("\n") end + end + end + + -- start at byte 1 in the string + dofs = 1 + + -- the first 24 bytes are expected to be an H2 preface on the request + if (dir == 1 and sofs < 24) then + -- let's not check it for now + local bytes = self.st[dir].tot - sofs + if (sofs + self.st[dir].tot >= 24) then + -- skip what was missing from the preface + dofs = dofs + 24 - sofs + sofs = 24 + io.write(pfx .. "[PREFACE len=24]\n") + else + -- consume more preface bytes + sofs = sofs + self.st[dir].tot + return + end + end + + -- parse contents as long as there are pending data + + while true do + -- check if we need to consume data from the current frame + -- flen is the number of bytes left before the frame's end. + if (self.st[dir].flen > 0) then + if dofs > #data then return end -- missing data + if (#data - dofs + 1 < self.st[dir].flen) then + -- insufficient data + self.st[dir].flen = self.st[dir].flen - (#data - dofs + 1) + io.write(pfx .. string.format("%32s\n", "... -" .. (#data - dofs + 1) .. " = " .. self.st[dir].flen)) + dofs = #data + 1 + return + else + -- enough data to finish + if (dofs == 1) then + -- only print a partial size if the frame was interrupted + io.write(pfx .. string.format("%32s\n", "... -" .. self.st[dir].flen .. " = 0")) + end + dofs = dofs + self.st[dir].flen + self.st[dir].flen = 0 + end + end + + -- here, flen = 0, we're at the beginning of a new frame -- + + -- read possibly missing header bytes until dec.fofs == 9 + while self.st[dir].fofs < 9 do + if dofs > #data then return end -- missing data + self.st[dir].hdr[self.st[dir].fofs + 1] = data:sub(dofs, dofs):byte() + dofs = dofs + 1 + self.st[dir].fofs = self.st[dir].fofs + 1 + end + + -- we have a full frame header here + if (self.do_hex ~= false) then + io.write("\n" .. pfx .. string.format("hdr=%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + self.st[dir].hdr[1], self.st[dir].hdr[2], self.st[dir].hdr[3], + self.st[dir].hdr[4], self.st[dir].hdr[5], self.st[dir].hdr[6], + self.st[dir].hdr[7], self.st[dir].hdr[8], self.st[dir].hdr[9])) + end + + -- we have a full frame header, we'll be ready + -- for a new frame once the data is gone + self.st[dir].flen = self.st[dir].hdr[1] * 65536 + + self.st[dir].hdr[2] * 256 + + self.st[dir].hdr[3] + self.st[dir].ftyp = self.st[dir].hdr[4] + self.st[dir].fflg = self.st[dir].hdr[5] + self.st[dir].sid = self.st[dir].hdr[6] * 16777216 + + self.st[dir].hdr[7] * 65536 + + self.st[dir].hdr[8] * 256 + + self.st[dir].hdr[9] + self.st[dir].fofs = 0 + + -- decode frame type + if self.st[dir].ftyp <= 9 then + ft = h2ft[self.st[dir].ftyp] + else + ft = string.format("TYPE_0x%02x\n", self.st[dir].ftyp) + end + + -- decode frame flags for frame type + ff = "" + for i = 7, 0, -1 do + if (((self.st[dir].fflg >> i) & 1) ~= 0) then + if h2ff[self.st[dir].ftyp][i] ~= nil then + ff = ff .. ((ff == "") and "" or "+") + ff = ff .. h2ff[self.st[dir].ftyp][i] + else + ff = ff .. ((ff == "") and "" or "+") + ff = ff .. string.format("0x%02x", 1<