vxx2 / vlib / net / http / h2_hpack.v
336 lines · 313 sloc · 10.21 KB · 89607c731290ee2c148c11541f73c40b23b00481
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6// This file implements HPACK header compression (RFC 7541), used by HTTP/2.
7// It is self-contained: it only depends on the static and Huffman tables in
8// the sibling h2_hpack_*.v files, and does not touch the rest of net.http.
9
10// h2_hpack_default_table_size is the default maximum size of the HPACK
11// dynamic table, in bytes (RFC 7541 Section 4.2, and the default value of
12// SETTINGS_HEADER_TABLE_SIZE).
13pub const h2_hpack_default_table_size = 4096
14
15// H2HeaderField is a single HTTP/2 header field (a name/value pair).
16// Field names are expected to be lowercase, as required by RFC 7540.
17pub struct H2HeaderField {
18pub:
19 name string
20 value string
21}
22
23// H2DynEntry is one entry in the HPACK dynamic table.
24struct H2DynEntry {
25 name string
26 value string
27 size int // name.len + value.len + 32 (RFC 7541 Section 4.1)
28}
29
30// H2DynTable is the HPACK dynamic table: a size-bounded FIFO of header fields.
31// entries[0] is always the most recently added entry, which corresponds to the
32// lowest dynamic index in the HPACK index space.
33struct H2DynTable {
34mut:
35 entries []H2DynEntry
36 cur_size int
37 max_size int = h2_hpack_default_table_size
38}
39
40// add inserts a new entry at the front of the table, evicting the oldest
41// entries until it fits. Per RFC 7541 Section 4.4, an entry larger than the
42// whole table empties it and is not added.
43fn (mut t H2DynTable) add(name string, value string) {
44 sz := name.len + value.len + 32
45 for t.entries.len > 0 && t.cur_size + sz > t.max_size {
46 t.cur_size -= t.entries.last().size
47 t.entries.delete_last()
48 }
49 if sz > t.max_size {
50 return
51 }
52 t.entries.insert(0, H2DynEntry{
53 name: name
54 value: value
55 size: sz
56 })
57 t.cur_size += sz
58}
59
60// set_max_size updates the maximum table size, evicting oldest entries as
61// needed to respect the new limit.
62fn (mut t H2DynTable) set_max_size(n int) {
63 t.max_size = n
64 for t.entries.len > 0 && t.cur_size > t.max_size {
65 t.cur_size -= t.entries.last().size
66 t.entries.delete_last()
67 }
68}
69
70// get returns the dynamic-table entry at 1-based dynamic index `i`
71// (i.e. i==1 is the newest entry), or none if out of range.
72fn (t &H2DynTable) get(i int) ?H2HeaderField {
73 if i < 1 || i > t.entries.len {
74 return none
75 }
76 e := t.entries[i - 1]
77 return H2HeaderField{
78 name: e.name
79 value: e.value
80 }
81}
82
83// H2HpackReader is a cursor over an HPACK header block being decoded.
84struct H2HpackReader {
85 buf []u8
86mut:
87 pos int
88}
89
90// read_int reads an HPACK variable-length integer with a prefix of
91// `prefix_bits` bits at the current position, advancing past it
92// (RFC 7541 Section 5.1).
93fn (mut r H2HpackReader) read_int(prefix_bits int) !u64 {
94 if r.pos >= r.buf.len {
95 return error('hpack: integer truncated')
96 }
97 max_prefix := u64((1 << prefix_bits) - 1)
98 mut value := u64(r.buf[r.pos]) & max_prefix
99 r.pos++
100 if value < max_prefix {
101 return value
102 }
103 mut m := 0
104 for {
105 if r.pos >= r.buf.len {
106 return error('hpack: integer continuation truncated')
107 }
108 b := r.buf[r.pos]
109 r.pos++
110 value += u64(b & 0x7f) << m
111 m += 7
112 // A 64-bit value never needs more than 9 continuation bytes; reject
113 // pathological inputs early (RFC 7541 Section 5.1 security note).
114 if m > 63 {
115 return error('hpack: integer overflow')
116 }
117 if b & 0x80 == 0 {
118 break
119 }
120 }
121 return value
122}
123
124// read_string reads an HPACK string literal at the current position,
125// advancing past it and decoding Huffman coding when the H bit is set.
126fn (mut r H2HpackReader) read_string() !string {
127 if r.pos >= r.buf.len {
128 return error('hpack: string truncated')
129 }
130 huffman := (r.buf[r.pos] & 0x80) != 0
131 length := r.read_int(7)!
132 // Compare in u64 space, before narrowing to int, so an oversized length
133 // from a malicious peer cannot truncate past this bounds check.
134 if length > u64(r.buf.len - r.pos) {
135 return error('hpack: string length exceeds buffer')
136 }
137 n := int(length)
138 raw := r.buf[r.pos..r.pos + n]
139 r.pos += n
140 if huffman {
141 return h2_huffman_decode(raw)!.bytestr()
142 }
143 return raw.bytestr()
144}
145
146// h2_hpack_write_int appends an HPACK variable-length integer to `out`, using
147// a prefix of `prefix_bits` bits whose high bits are set from `high_bits`.
148fn h2_hpack_write_int(mut out []u8, value u64, prefix_bits int, high_bits u8) {
149 max_prefix := u64((1 << prefix_bits) - 1)
150 if value < max_prefix {
151 out << high_bits | u8(value)
152 return
153 }
154 out << high_bits | u8(max_prefix)
155 mut v := value - max_prefix
156 for v >= 0x80 {
157 out << u8((v & 0x7f) | 0x80)
158 v >>= 7
159 }
160 out << u8(v)
161}
162
163// h2_encode_string appends an HPACK string literal to `out`, choosing Huffman
164// coding when it is shorter than the raw bytes (RFC 7541 Section 5.2).
165fn h2_encode_string(mut out []u8, s string) {
166 raw := s.bytes()
167 huff := h2_huffman_encode(raw)
168 if huff.len < raw.len {
169 h2_hpack_write_int(mut out, u64(huff.len), 7, 0x80) // H = 1
170 out << huff
171 } else {
172 h2_hpack_write_int(mut out, u64(raw.len), 7, 0x00) // H = 0
173 out << raw
174 }
175}
176
177// h2_is_sensitive reports whether a header should never be added to the HPACK
178// dynamic table (encoded as "never indexed"), to avoid CRIME-style attacks.
179fn h2_is_sensitive(name string) bool {
180 return name == 'cookie' || name == 'authorization' || name == 'proxy-authorization'
181}
182
183// h2_hpack_find_static searches the static table for `name`/`value`.
184// It returns the 1-based HPACK index and whether the value also matched.
185// The index is 0 when the name is not present at all.
186fn h2_hpack_find_static(name string, value string) (int, bool) {
187 mut name_idx := 0
188 for i, f in h2_hpack_static_table {
189 if f.name == name {
190 if f.value == value {
191 return i + 1, true
192 }
193 if name_idx == 0 {
194 name_idx = i + 1
195 }
196 }
197 }
198 return name_idx, false
199}
200
201// H2HpackEncoder encodes header field lists into HPACK header blocks.
202// This encoder never adds entries to the dynamic table: it uses indexed
203// representations for static-table hits and literal-without-indexing (or
204// never-indexed, for sensitive headers) otherwise. That keeps encoder and
205// decoder state trivially in sync while remaining fully interoperable.
206pub struct H2HpackEncoder {
207pub mut:
208 dyn_table H2DynTable
209 pending_max_table_size int = -1 // -1 = no pending update; ≥0 = emit size update on next encode
210}
211
212// encode returns the HPACK header block for `fields`.
213pub fn (mut e H2HpackEncoder) encode(fields []H2HeaderField) []u8 {
214 mut out := []u8{}
215 if e.pending_max_table_size >= 0 {
216 // RFC 7541 §6.3: emit Dynamic Table Size Update at the start of the
217 // first header block after a peer-requested table size change.
218 h2_hpack_write_int(mut out, u64(e.pending_max_table_size), 5, 0x20)
219 e.pending_max_table_size = -1
220 }
221 for f in fields {
222 e.encode_field(mut out, f)
223 }
224 return out
225}
226
227fn (mut e H2HpackEncoder) encode_field(mut out []u8, f H2HeaderField) {
228 sensitive := h2_is_sensitive(f.name)
229 idx, exact := h2_hpack_find_static(f.name, f.value)
230 if exact && !sensitive {
231 // Indexed Header Field (RFC 7541 Section 6.1).
232 h2_hpack_write_int(mut out, u64(idx), 7, 0x80)
233 return
234 }
235 // Literal Header Field without Indexing (6.2.2) or Never Indexed (6.2.3).
236 pattern := if sensitive { u8(0x10) } else { u8(0x00) }
237 if idx != 0 {
238 h2_hpack_write_int(mut out, u64(idx), 4, pattern)
239 } else {
240 h2_hpack_write_int(mut out, 0, 4, pattern)
241 h2_encode_string(mut out, f.name)
242 }
243 h2_encode_string(mut out, f.value)
244}
245
246// H2HpackDecoder decodes HPACK header blocks into header field lists,
247// maintaining the dynamic table across calls on the same connection.
248pub struct H2HpackDecoder {
249pub mut:
250 dyn_table H2DynTable
251 max_dynamic_size int = h2_hpack_default_table_size // upper bound we advertised to the peer
252}
253
254// lookup resolves a 1-based HPACK index against the static and dynamic tables.
255fn (d &H2HpackDecoder) lookup(idx u64) !H2HeaderField {
256 if idx == 0 {
257 return error('hpack: index 0 is not valid')
258 }
259 if idx <= u64(h2_hpack_static_len) {
260 return h2_hpack_static_table[idx - 1]
261 }
262 // Validate the dynamic index in u64 space before narrowing to int, so an
263 // out-of-range index from a malicious peer cannot truncate onto a valid
264 // dynamic entry.
265 dyn_idx := idx - u64(h2_hpack_static_len)
266 if dyn_idx > u64(d.dyn_table.entries.len) {
267 return error('hpack: index ${idx} out of range')
268 }
269 return d.dyn_table.get(int(dyn_idx)) or { error('hpack: index ${idx} out of range') }
270}
271
272fn (d &H2HpackDecoder) read_literal(mut r H2HpackReader, prefix_bits int) !H2HeaderField {
273 name_index := r.read_int(prefix_bits)!
274 name := if name_index == 0 {
275 r.read_string()!
276 } else {
277 d.lookup(name_index)!.name
278 }
279 value := r.read_string()!
280 return H2HeaderField{
281 name: name
282 value: value
283 }
284}
285
286// decode parses one HPACK header block and returns its header fields,
287// updating the dynamic table as instructed by the block.
288pub fn (mut d H2HpackDecoder) decode(block []u8) ![]H2HeaderField {
289 mut out := []H2HeaderField{}
290 mut r := H2HpackReader{
291 buf: block
292 }
293 mut seen_field := false
294 for r.pos < block.len {
295 b := block[r.pos]
296 if b & 0x80 != 0 {
297 // Indexed Header Field (RFC 7541 Section 6.1).
298 idx := r.read_int(7)!
299 out << d.lookup(idx)!
300 seen_field = true
301 } else if b & 0x40 != 0 {
302 // Literal Header Field with Incremental Indexing (6.2.1).
303 f := d.read_literal(mut r, 6)!
304 d.dyn_table.add(f.name, f.value)
305 out << f
306 seen_field = true
307 } else if b & 0x20 != 0 {
308 // Dynamic Table Size Update (6.3). Must precede any header field.
309 if seen_field {
310 return error('hpack: dynamic table size update after a header field')
311 }
312 new_size := r.read_int(5)!
313 if int(new_size) > d.max_dynamic_size {
314 return error('hpack: dynamic table size update ${new_size} exceeds limit ${d.max_dynamic_size}')
315 }
316 d.dyn_table.set_max_size(int(new_size))
317 } else {
318 // Literal without Indexing (6.2.2) or Never Indexed (6.2.3);
319 // neither updates the dynamic table.
320 f := d.read_literal(mut r, 4)!
321 out << f
322 seen_field = true
323 }
324 }
325 return out
326}
327
328// set_max_dynamic_size updates the maximum dynamic-table size the decoder will
329// accept (i.e. the SETTINGS_HEADER_TABLE_SIZE value advertised to the peer),
330// shrinking the table immediately if needed.
331pub fn (mut d H2HpackDecoder) set_max_dynamic_size(n int) {
332 d.max_dynamic_size = n
333 if d.dyn_table.max_size > n {
334 d.dyn_table.set_max_size(n)
335 }
336}
337