v / vlib / net / http / h2_hpack.v
329 lines · 306 sloc · 9.82 KB · c5fca67313f5184e98214e7af2b4a553e66a9b3d
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}
210
211// encode returns the HPACK header block for `fields`.
212pub fn (mut e H2HpackEncoder) encode(fields []H2HeaderField) []u8 {
213 mut out := []u8{}
214 for f in fields {
215 e.encode_field(mut out, f)
216 }
217 return out
218}
219
220fn (mut e H2HpackEncoder) encode_field(mut out []u8, f H2HeaderField) {
221 sensitive := h2_is_sensitive(f.name)
222 idx, exact := h2_hpack_find_static(f.name, f.value)
223 if exact && !sensitive {
224 // Indexed Header Field (RFC 7541 Section 6.1).
225 h2_hpack_write_int(mut out, u64(idx), 7, 0x80)
226 return
227 }
228 // Literal Header Field without Indexing (6.2.2) or Never Indexed (6.2.3).
229 pattern := if sensitive { u8(0x10) } else { u8(0x00) }
230 if idx != 0 {
231 h2_hpack_write_int(mut out, u64(idx), 4, pattern)
232 } else {
233 h2_hpack_write_int(mut out, 0, 4, pattern)
234 h2_encode_string(mut out, f.name)
235 }
236 h2_encode_string(mut out, f.value)
237}
238
239// H2HpackDecoder decodes HPACK header blocks into header field lists,
240// maintaining the dynamic table across calls on the same connection.
241pub struct H2HpackDecoder {
242pub mut:
243 dyn_table H2DynTable
244 max_dynamic_size int = h2_hpack_default_table_size // upper bound we advertised to the peer
245}
246
247// lookup resolves a 1-based HPACK index against the static and dynamic tables.
248fn (d &H2HpackDecoder) lookup(idx u64) !H2HeaderField {
249 if idx == 0 {
250 return error('hpack: index 0 is not valid')
251 }
252 if idx <= u64(h2_hpack_static_len) {
253 return h2_hpack_static_table[idx - 1]
254 }
255 // Validate the dynamic index in u64 space before narrowing to int, so an
256 // out-of-range index from a malicious peer cannot truncate onto a valid
257 // dynamic entry.
258 dyn_idx := idx - u64(h2_hpack_static_len)
259 if dyn_idx > u64(d.dyn_table.entries.len) {
260 return error('hpack: index ${idx} out of range')
261 }
262 return d.dyn_table.get(int(dyn_idx)) or { error('hpack: index ${idx} out of range') }
263}
264
265fn (d &H2HpackDecoder) read_literal(mut r H2HpackReader, prefix_bits int) !H2HeaderField {
266 name_index := r.read_int(prefix_bits)!
267 name := if name_index == 0 {
268 r.read_string()!
269 } else {
270 d.lookup(name_index)!.name
271 }
272 value := r.read_string()!
273 return H2HeaderField{
274 name: name
275 value: value
276 }
277}
278
279// decode parses one HPACK header block and returns its header fields,
280// updating the dynamic table as instructed by the block.
281pub fn (mut d H2HpackDecoder) decode(block []u8) ![]H2HeaderField {
282 mut out := []H2HeaderField{}
283 mut r := H2HpackReader{
284 buf: block
285 }
286 mut seen_field := false
287 for r.pos < block.len {
288 b := block[r.pos]
289 if b & 0x80 != 0 {
290 // Indexed Header Field (RFC 7541 Section 6.1).
291 idx := r.read_int(7)!
292 out << d.lookup(idx)!
293 seen_field = true
294 } else if b & 0x40 != 0 {
295 // Literal Header Field with Incremental Indexing (6.2.1).
296 f := d.read_literal(mut r, 6)!
297 d.dyn_table.add(f.name, f.value)
298 out << f
299 seen_field = true
300 } else if b & 0x20 != 0 {
301 // Dynamic Table Size Update (6.3). Must precede any header field.
302 if seen_field {
303 return error('hpack: dynamic table size update after a header field')
304 }
305 new_size := r.read_int(5)!
306 if int(new_size) > d.max_dynamic_size {
307 return error('hpack: dynamic table size update ${new_size} exceeds limit ${d.max_dynamic_size}')
308 }
309 d.dyn_table.set_max_size(int(new_size))
310 } else {
311 // Literal without Indexing (6.2.2) or Never Indexed (6.2.3);
312 // neither updates the dynamic table.
313 f := d.read_literal(mut r, 4)!
314 out << f
315 seen_field = true
316 }
317 }
318 return out
319}
320
321// set_max_dynamic_size updates the maximum dynamic-table size the decoder will
322// accept (i.e. the SETTINGS_HEADER_TABLE_SIZE value advertised to the peer),
323// shrinking the table immediately if needed.
324pub fn (mut d H2HpackDecoder) set_max_dynamic_size(n int) {
325 d.max_dynamic_size = n
326 if d.dyn_table.max_size > n {
327 d.dyn_table.set_max_size(n)
328 }
329}
330