File: h2-tracer.lua

package info (click to toggle)
haproxy 3.2.8-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 23,880 kB
  • sloc: ansic: 267,692; sh: 3,277; xml: 1,756; python: 1,345; makefile: 1,155; perl: 168; cpp: 21
file content (247 lines) | stat: -rw-r--r-- 7,982 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
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] = { [2] = "EH" }, -- 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 <data> 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 <ftyp>
        ff = ""
        for i = 7, 0, -1 do
            if (((self.st[dir].fflg >> i) & 1) ~= 0) then
                if self.st[dir].ftyp <= 9 and 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<<i)
                end
            end
        end

        io.write(pfx .. string.format("[%s %ssid=%u len=%u (bytes=%u)]\n",
            ft, (ff == "") and "" or ff .. " ",
            self.st[dir].sid, self.st[dir].flen,
            (#data - dofs + 1)))
    end
end

core.register_filter("h2-tracer", Dec, function(dec, args)
    Dec.args = args
    return dec
end)