Skip to content

Commit c72e89d

Browse files
neildgopherbot
authored andcommitted
internal/http3: QPACK encoding and decoding
Basic support for encoding/decoding QPACK headers. QPACK supports three forms of header compression: Huffman-encoding of literal strings, a static table of well-known header values, and a dynamic table of header values negotiated between encoder and decoder at runtime. Right now, we support Huffman compression and the static table, but not the dynamic table. This is a supported mode for a QPACK encoder or decoder, so we can leave dynamic table support for after the rest of HTTP/3 is working. For golang/go#70914 Change-Id: Ib694199b99c752a220d43f3a309169b16020b474 Reviewed-on: https://go-review.googlesource.com/c/net/+/642599 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Damien Neil <dneil@google.com> Reviewed-by: Jonathan Amsterdam <jba@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
1 parent 93c1957 commit c72e89d

8 files changed

+795
-2
lines changed

internal/http3/http3_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
package http3
88

99
import (
10+
"encoding/hex"
1011
"os"
1112
"slices"
13+
"strings"
1214
"testing"
1315
"testing/synctest"
1416
)
@@ -57,3 +59,17 @@ func (t *cleanupT) done() {
5759
f()
5860
}
5961
}
62+
63+
func unhex(s string) []byte {
64+
b, err := hex.DecodeString(strings.Map(func(c rune) rune {
65+
switch c {
66+
case ' ', '\t', '\n':
67+
return -1 // ignore
68+
}
69+
return c
70+
}, s))
71+
if err != nil {
72+
panic(err)
73+
}
74+
return b
75+
}

internal/http3/qpack.go

+182-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,190 @@
77
package http3
88

99
import (
10+
"errors"
1011
"io"
1112

1213
"golang.org/x/net/http2/hpack"
1314
)
1415

16+
// QPACK (RFC 9204) header compression wire encoding.
17+
// https://www.rfc-editor.org/rfc/rfc9204.html
18+
19+
// tableType is the static or dynamic table.
20+
//
21+
// The T bit in QPACK instructions indicates whether a table index refers to
22+
// the dynamic (T=0) or static (T=1) table. tableTypeForTBit and tableType.tbit
23+
// convert a T bit from the wire encoding to/from a tableType.
24+
type tableType byte
25+
26+
const (
27+
dynamicTable = 0x00 // T=0, dynamic table
28+
staticTable = 0xff // T=1, static table
29+
)
30+
31+
// tableTypeForTbit returns the table type corresponding to a T bit value.
32+
// The input parameter contains a byte masked to contain only the T bit.
33+
func tableTypeForTbit(bit byte) tableType {
34+
if bit == 0 {
35+
return dynamicTable
36+
}
37+
return staticTable
38+
}
39+
40+
// tbit produces the T bit corresponding to the table type.
41+
// The input parameter contains a byte with the T bit set to 1,
42+
// and the return is either the input or 0 depending on the table type.
43+
func (t tableType) tbit(bit byte) byte {
44+
return bit & byte(t)
45+
}
46+
47+
// indexType indicates a literal's indexing status.
48+
//
49+
// The N bit in QPACK instructions indicates whether a literal is "never-indexed".
50+
// A never-indexed literal (N=1) must not be encoded as an indexed literal if it
51+
// forwarded on another connection.
52+
//
53+
// (See https://www.rfc-editor.org/rfc/rfc9204.html#section-7.1 for details on the
54+
// security reasons for never-indexed literals.)
55+
type indexType byte
56+
57+
const (
58+
mayIndex = 0x00 // N=0, not a never-indexed literal
59+
neverIndex = 0xff // N=1, never-indexed literal
60+
)
61+
62+
// indexTypeForNBit returns the index type corresponding to a N bit value.
63+
// The input parameter contains a byte masked to contain only the N bit.
64+
func indexTypeForNBit(bit byte) indexType {
65+
if bit == 0 {
66+
return mayIndex
67+
}
68+
return neverIndex
69+
}
70+
71+
// nbit produces the N bit corresponding to the table type.
72+
// The input parameter contains a byte with the N bit set to 1,
73+
// and the return is either the input or 0 depending on the table type.
74+
func (t indexType) nbit(bit byte) byte {
75+
return bit & byte(t)
76+
}
77+
78+
// Indexed Field Line:
79+
//
80+
// 0 1 2 3 4 5 6 7
81+
// +---+---+---+---+---+---+---+---+
82+
// | 1 | T | Index (6+) |
83+
// +---+---+-----------------------+
84+
//
85+
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.2
86+
87+
func appendIndexedFieldLine(b []byte, ttype tableType, index int) []byte {
88+
const tbit = 0b_01000000
89+
return appendPrefixedInt(b, 0b_1000_0000|ttype.tbit(tbit), 6, int64(index))
90+
}
91+
92+
func (st *stream) decodeIndexedFieldLine(b byte) (itype indexType, name, value string, err error) {
93+
index, err := st.readPrefixedIntWithByte(b, 6)
94+
if err != nil {
95+
return 0, "", "", err
96+
}
97+
const tbit = 0b_0100_0000
98+
if tableTypeForTbit(b&tbit) == staticTable {
99+
ent, err := staticTableEntry(index)
100+
if err != nil {
101+
return 0, "", "", err
102+
}
103+
return mayIndex, ent.name, ent.value, nil
104+
} else {
105+
return 0, "", "", errors.New("dynamic table is not supported yet")
106+
}
107+
}
108+
109+
// Literal Field Line With Name Reference:
110+
//
111+
// 0 1 2 3 4 5 6 7
112+
// +---+---+---+---+---+---+---+---+
113+
// | 0 | 1 | N | T |Name Index (4+)|
114+
// +---+---+---+---+---------------+
115+
// | H | Value Length (7+) |
116+
// +---+---------------------------+
117+
// | Value String (Length bytes) |
118+
// +-------------------------------+
119+
//
120+
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.4
121+
122+
func appendLiteralFieldLineWithNameReference(b []byte, ttype tableType, itype indexType, nameIndex int, value string) []byte {
123+
const tbit = 0b_0001_0000
124+
const nbit = 0b_0010_0000
125+
b = appendPrefixedInt(b, 0b_0100_0000|itype.nbit(nbit)|ttype.tbit(tbit), 4, int64(nameIndex))
126+
b = appendPrefixedString(b, 0, 7, value)
127+
return b
128+
}
129+
130+
func (st *stream) decodeLiteralFieldLineWithNameReference(b byte) (itype indexType, name, value string, err error) {
131+
nameIndex, err := st.readPrefixedIntWithByte(b, 4)
132+
if err != nil {
133+
return 0, "", "", err
134+
}
135+
136+
const tbit = 0b_0001_0000
137+
if tableTypeForTbit(b&tbit) == staticTable {
138+
ent, err := staticTableEntry(nameIndex)
139+
if err != nil {
140+
return 0, "", "", err
141+
}
142+
name = ent.name
143+
} else {
144+
return 0, "", "", errors.New("dynamic table is not supported yet")
145+
}
146+
147+
_, value, err = st.readPrefixedString(7)
148+
if err != nil {
149+
return 0, "", "", err
150+
}
151+
152+
const nbit = 0b_0010_0000
153+
itype = indexTypeForNBit(b & nbit)
154+
155+
return itype, name, value, nil
156+
}
157+
158+
// Literal Field Line with Literal Name:
159+
//
160+
// 0 1 2 3 4 5 6 7
161+
// +---+---+---+---+---+---+---+---+
162+
// | 0 | 0 | 1 | N | H |NameLen(3+)|
163+
// +---+---+---+---+---+-----------+
164+
// | Name String (Length bytes) |
165+
// +---+---------------------------+
166+
// | H | Value Length (7+) |
167+
// +---+---------------------------+
168+
// | Value String (Length bytes) |
169+
// +-------------------------------+
170+
//
171+
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.6
172+
173+
func appendLiteralFieldLineWithLiteralName(b []byte, itype indexType, name, value string) []byte {
174+
const nbit = 0b_0001_0000
175+
b = appendPrefixedString(b, 0b_0010_0000|itype.nbit(nbit), 3, name)
176+
b = appendPrefixedString(b, 0, 7, value)
177+
return b
178+
}
179+
180+
func (st *stream) decodeLiteralFieldLineWithLiteralName(b byte) (itype indexType, name, value string, err error) {
181+
name, err = st.readPrefixedStringWithByte(b, 3)
182+
if err != nil {
183+
return 0, "", "", err
184+
}
185+
_, value, err = st.readPrefixedString(7)
186+
if err != nil {
187+
return 0, "", "", err
188+
}
189+
const nbit = 0b_0001_0000
190+
itype = indexTypeForNBit(b & nbit)
191+
return itype, name, value, nil
192+
}
193+
15194
// Prefixed-integer encoding from RFC 7541, section 5.1
16195
//
17196
// Prefixed integers consist of some number of bits of data,
@@ -135,7 +314,9 @@ func (st *stream) readPrefixedStringWithByte(firstByte byte, prefixLen uint8) (s
135314
return string(data), nil
136315
}
137316

138-
// appendPrefixedString appends an RFC 7541 string to st.
317+
// appendPrefixedString appends an RFC 7541 string to st,
318+
// applying Huffman encoding and setting the H bit (indicating Huffman encoding)
319+
// when appropriate.
139320
//
140321
// The firstByte parameter includes the non-integer bits of the first byte.
141322
// The other bits must be zero.

internal/http3/qpack_decode.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.24
6+
7+
package http3
8+
9+
import (
10+
"errors"
11+
"math/bits"
12+
)
13+
14+
type qpackDecoder struct {
15+
// The decoder has no state for now,
16+
// but that'll change once we add dynamic table support.
17+
//
18+
// TODO: dynamic table support.
19+
}
20+
21+
func (qd *qpackDecoder) decode(st *stream, f func(itype indexType, name, value string) error) error {
22+
// Encoded Field Section prefix.
23+
24+
// We set SETTINGS_QPACK_MAX_TABLE_CAPACITY to 0,
25+
// so the Required Insert Count must be 0.
26+
_, requiredInsertCount, err := st.readPrefixedInt(8)
27+
if err != nil {
28+
return err
29+
}
30+
if requiredInsertCount != 0 {
31+
return errQPACKDecompressionFailed
32+
}
33+
34+
// Delta Base. We don't use the dynamic table yet, so this may be ignored.
35+
_, _, err = st.readPrefixedInt(7)
36+
if err != nil {
37+
return err
38+
}
39+
40+
sawNonPseudo := false
41+
for st.lim > 0 {
42+
firstByte, err := st.ReadByte()
43+
if err != nil {
44+
return err
45+
}
46+
var name, value string
47+
var itype indexType
48+
switch bits.LeadingZeros8(firstByte) {
49+
case 0:
50+
// Indexed Field Line
51+
itype, name, value, err = st.decodeIndexedFieldLine(firstByte)
52+
case 1:
53+
// Literal Field Line With Name Reference
54+
itype, name, value, err = st.decodeLiteralFieldLineWithNameReference(firstByte)
55+
case 2:
56+
// Literal Field Line with Literal Name
57+
itype, name, value, err = st.decodeLiteralFieldLineWithLiteralName(firstByte)
58+
case 3:
59+
// Indexed Field Line With Post-Base Index
60+
err = errors.New("dynamic table is not supported yet")
61+
case 4:
62+
// Indexed Field Line With Post-Base Name Reference
63+
err = errors.New("dynamic table is not supported yet")
64+
}
65+
if err != nil {
66+
return err
67+
}
68+
if len(name) == 0 {
69+
return errH3MessageError
70+
}
71+
if name[0] == ':' {
72+
if sawNonPseudo {
73+
return errH3MessageError
74+
}
75+
} else {
76+
sawNonPseudo = true
77+
}
78+
if err := f(itype, name, value); err != nil {
79+
return err
80+
}
81+
}
82+
return nil
83+
}

0 commit comments

Comments
 (0)