mirror of
https://github.com/golang/net.git
synced 2026-04-01 02:47:08 +09:00
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>
This commit is contained in:
committed by
Gopher Robot
parent
93c1957e42
commit
c72e89d6a9
@@ -7,8 +7,10 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
)
|
||||
@@ -57,3 +59,17 @@ func (t *cleanupT) done() {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
func unhex(s string) []byte {
|
||||
b, err := hex.DecodeString(strings.Map(func(c rune) rune {
|
||||
switch c {
|
||||
case ' ', '\t', '\n':
|
||||
return -1 // ignore
|
||||
}
|
||||
return c
|
||||
}, s))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -7,11 +7,190 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"golang.org/x/net/http2/hpack"
|
||||
)
|
||||
|
||||
// QPACK (RFC 9204) header compression wire encoding.
|
||||
// https://www.rfc-editor.org/rfc/rfc9204.html
|
||||
|
||||
// tableType is the static or dynamic table.
|
||||
//
|
||||
// The T bit in QPACK instructions indicates whether a table index refers to
|
||||
// the dynamic (T=0) or static (T=1) table. tableTypeForTBit and tableType.tbit
|
||||
// convert a T bit from the wire encoding to/from a tableType.
|
||||
type tableType byte
|
||||
|
||||
const (
|
||||
dynamicTable = 0x00 // T=0, dynamic table
|
||||
staticTable = 0xff // T=1, static table
|
||||
)
|
||||
|
||||
// tableTypeForTbit returns the table type corresponding to a T bit value.
|
||||
// The input parameter contains a byte masked to contain only the T bit.
|
||||
func tableTypeForTbit(bit byte) tableType {
|
||||
if bit == 0 {
|
||||
return dynamicTable
|
||||
}
|
||||
return staticTable
|
||||
}
|
||||
|
||||
// tbit produces the T bit corresponding to the table type.
|
||||
// The input parameter contains a byte with the T bit set to 1,
|
||||
// and the return is either the input or 0 depending on the table type.
|
||||
func (t tableType) tbit(bit byte) byte {
|
||||
return bit & byte(t)
|
||||
}
|
||||
|
||||
// indexType indicates a literal's indexing status.
|
||||
//
|
||||
// The N bit in QPACK instructions indicates whether a literal is "never-indexed".
|
||||
// A never-indexed literal (N=1) must not be encoded as an indexed literal if it
|
||||
// forwarded on another connection.
|
||||
//
|
||||
// (See https://www.rfc-editor.org/rfc/rfc9204.html#section-7.1 for details on the
|
||||
// security reasons for never-indexed literals.)
|
||||
type indexType byte
|
||||
|
||||
const (
|
||||
mayIndex = 0x00 // N=0, not a never-indexed literal
|
||||
neverIndex = 0xff // N=1, never-indexed literal
|
||||
)
|
||||
|
||||
// indexTypeForNBit returns the index type corresponding to a N bit value.
|
||||
// The input parameter contains a byte masked to contain only the N bit.
|
||||
func indexTypeForNBit(bit byte) indexType {
|
||||
if bit == 0 {
|
||||
return mayIndex
|
||||
}
|
||||
return neverIndex
|
||||
}
|
||||
|
||||
// nbit produces the N bit corresponding to the table type.
|
||||
// The input parameter contains a byte with the N bit set to 1,
|
||||
// and the return is either the input or 0 depending on the table type.
|
||||
func (t indexType) nbit(bit byte) byte {
|
||||
return bit & byte(t)
|
||||
}
|
||||
|
||||
// Indexed Field Line:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | T | Index (6+) |
|
||||
// +---+---+-----------------------+
|
||||
//
|
||||
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.2
|
||||
|
||||
func appendIndexedFieldLine(b []byte, ttype tableType, index int) []byte {
|
||||
const tbit = 0b_01000000
|
||||
return appendPrefixedInt(b, 0b_1000_0000|ttype.tbit(tbit), 6, int64(index))
|
||||
}
|
||||
|
||||
func (st *stream) decodeIndexedFieldLine(b byte) (itype indexType, name, value string, err error) {
|
||||
index, err := st.readPrefixedIntWithByte(b, 6)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
const tbit = 0b_0100_0000
|
||||
if tableTypeForTbit(b&tbit) == staticTable {
|
||||
ent, err := staticTableEntry(index)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
return mayIndex, ent.name, ent.value, nil
|
||||
} else {
|
||||
return 0, "", "", errors.New("dynamic table is not supported yet")
|
||||
}
|
||||
}
|
||||
|
||||
// Literal Field Line With Name Reference:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 1 | N | T |Name Index (4+)|
|
||||
// +---+---+---+---+---------------+
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length bytes) |
|
||||
// +-------------------------------+
|
||||
//
|
||||
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.4
|
||||
|
||||
func appendLiteralFieldLineWithNameReference(b []byte, ttype tableType, itype indexType, nameIndex int, value string) []byte {
|
||||
const tbit = 0b_0001_0000
|
||||
const nbit = 0b_0010_0000
|
||||
b = appendPrefixedInt(b, 0b_0100_0000|itype.nbit(nbit)|ttype.tbit(tbit), 4, int64(nameIndex))
|
||||
b = appendPrefixedString(b, 0, 7, value)
|
||||
return b
|
||||
}
|
||||
|
||||
func (st *stream) decodeLiteralFieldLineWithNameReference(b byte) (itype indexType, name, value string, err error) {
|
||||
nameIndex, err := st.readPrefixedIntWithByte(b, 4)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
|
||||
const tbit = 0b_0001_0000
|
||||
if tableTypeForTbit(b&tbit) == staticTable {
|
||||
ent, err := staticTableEntry(nameIndex)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
name = ent.name
|
||||
} else {
|
||||
return 0, "", "", errors.New("dynamic table is not supported yet")
|
||||
}
|
||||
|
||||
_, value, err = st.readPrefixedString(7)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
|
||||
const nbit = 0b_0010_0000
|
||||
itype = indexTypeForNBit(b & nbit)
|
||||
|
||||
return itype, name, value, nil
|
||||
}
|
||||
|
||||
// Literal Field Line with Literal Name:
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 1 | N | H |NameLen(3+)|
|
||||
// +---+---+---+---+---+-----------+
|
||||
// | Name String (Length bytes) |
|
||||
// +---+---------------------------+
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length bytes) |
|
||||
// +-------------------------------+
|
||||
//
|
||||
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.5.6
|
||||
|
||||
func appendLiteralFieldLineWithLiteralName(b []byte, itype indexType, name, value string) []byte {
|
||||
const nbit = 0b_0001_0000
|
||||
b = appendPrefixedString(b, 0b_0010_0000|itype.nbit(nbit), 3, name)
|
||||
b = appendPrefixedString(b, 0, 7, value)
|
||||
return b
|
||||
}
|
||||
|
||||
func (st *stream) decodeLiteralFieldLineWithLiteralName(b byte) (itype indexType, name, value string, err error) {
|
||||
name, err = st.readPrefixedStringWithByte(b, 3)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
_, value, err = st.readPrefixedString(7)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
const nbit = 0b_0001_0000
|
||||
itype = indexTypeForNBit(b & nbit)
|
||||
return itype, name, value, nil
|
||||
}
|
||||
|
||||
// Prefixed-integer encoding from RFC 7541, section 5.1
|
||||
//
|
||||
// Prefixed integers consist of some number of bits of data,
|
||||
@@ -135,7 +314,9 @@ func (st *stream) readPrefixedStringWithByte(firstByte byte, prefixLen uint8) (s
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// appendPrefixedString appends an RFC 7541 string to st.
|
||||
// appendPrefixedString appends an RFC 7541 string to st,
|
||||
// applying Huffman encoding and setting the H bit (indicating Huffman encoding)
|
||||
// when appropriate.
|
||||
//
|
||||
// The firstByte parameter includes the non-integer bits of the first byte.
|
||||
// The other bits must be zero.
|
||||
|
||||
83
internal/http3/qpack_decode.go
Normal file
83
internal/http3/qpack_decode.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.24
|
||||
|
||||
package http3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
type qpackDecoder struct {
|
||||
// The decoder has no state for now,
|
||||
// but that'll change once we add dynamic table support.
|
||||
//
|
||||
// TODO: dynamic table support.
|
||||
}
|
||||
|
||||
func (qd *qpackDecoder) decode(st *stream, f func(itype indexType, name, value string) error) error {
|
||||
// Encoded Field Section prefix.
|
||||
|
||||
// We set SETTINGS_QPACK_MAX_TABLE_CAPACITY to 0,
|
||||
// so the Required Insert Count must be 0.
|
||||
_, requiredInsertCount, err := st.readPrefixedInt(8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if requiredInsertCount != 0 {
|
||||
return errQPACKDecompressionFailed
|
||||
}
|
||||
|
||||
// Delta Base. We don't use the dynamic table yet, so this may be ignored.
|
||||
_, _, err = st.readPrefixedInt(7)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sawNonPseudo := false
|
||||
for st.lim > 0 {
|
||||
firstByte, err := st.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var name, value string
|
||||
var itype indexType
|
||||
switch bits.LeadingZeros8(firstByte) {
|
||||
case 0:
|
||||
// Indexed Field Line
|
||||
itype, name, value, err = st.decodeIndexedFieldLine(firstByte)
|
||||
case 1:
|
||||
// Literal Field Line With Name Reference
|
||||
itype, name, value, err = st.decodeLiteralFieldLineWithNameReference(firstByte)
|
||||
case 2:
|
||||
// Literal Field Line with Literal Name
|
||||
itype, name, value, err = st.decodeLiteralFieldLineWithLiteralName(firstByte)
|
||||
case 3:
|
||||
// Indexed Field Line With Post-Base Index
|
||||
err = errors.New("dynamic table is not supported yet")
|
||||
case 4:
|
||||
// Indexed Field Line With Post-Base Name Reference
|
||||
err = errors.New("dynamic table is not supported yet")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return errH3MessageError
|
||||
}
|
||||
if name[0] == ':' {
|
||||
if sawNonPseudo {
|
||||
return errH3MessageError
|
||||
}
|
||||
} else {
|
||||
sawNonPseudo = true
|
||||
}
|
||||
if err := f(itype, name, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
196
internal/http3/qpack_decode_test.go
Normal file
196
internal/http3/qpack_decode_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.24
|
||||
|
||||
package http3
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQPACKDecode(t *testing.T) {
|
||||
type header struct {
|
||||
itype indexType
|
||||
name, value string
|
||||
}
|
||||
// Many test cases here taken from Google QUICHE,
|
||||
// quiche/quic/core/qpack/qpack_encoder_test.cc.
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
enc []byte
|
||||
want []header
|
||||
}{{
|
||||
name: "empty",
|
||||
enc: unhex("0000"),
|
||||
want: []header{},
|
||||
}, {
|
||||
name: "literal entry empty value",
|
||||
enc: unhex("000023666f6f00"),
|
||||
want: []header{
|
||||
{mayIndex, "foo", ""},
|
||||
},
|
||||
}, {
|
||||
name: "simple literal entry",
|
||||
enc: unhex("000023666f6f03626172"),
|
||||
want: []header{
|
||||
{mayIndex, "foo", "bar"},
|
||||
},
|
||||
}, {
|
||||
name: "multiple literal entries",
|
||||
enc: unhex("0000" + // prefix
|
||||
// foo: bar
|
||||
"23666f6f03626172" +
|
||||
// 7 octet long header name, the smallest number
|
||||
// that does not fit on a 3-bit prefix.
|
||||
"2700666f6f62616172" +
|
||||
// 127 octet long header value, the smallest number
|
||||
// that does not fit on a 7-bit prefix.
|
||||
"7f00616161616161616161616161616161616161616161616161616161616161616161" +
|
||||
"6161616161616161616161616161616161616161616161616161616161616161616161" +
|
||||
"6161616161616161616161616161616161616161616161616161616161616161616161" +
|
||||
"616161616161616161616161616161616161616161616161",
|
||||
),
|
||||
want: []header{
|
||||
{mayIndex, "foo", "bar"},
|
||||
{mayIndex, "foobaar", strings.Repeat("a", 127)},
|
||||
},
|
||||
}, {
|
||||
name: "line feed in value",
|
||||
enc: unhex("000023666f6f0462610a72"),
|
||||
want: []header{
|
||||
{mayIndex, "foo", "ba\nr"},
|
||||
},
|
||||
}, {
|
||||
name: "huffman simple",
|
||||
enc: unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4bf"),
|
||||
want: []header{
|
||||
{mayIndex, "custom-key", "custom-value"},
|
||||
},
|
||||
}, {
|
||||
name: "alternating huffman nonhuffman",
|
||||
enc: unhex("0000" + // Prefix.
|
||||
"2f0125a849e95ba97d7f" + // Huffman-encoded name.
|
||||
"8925a849e95bb8e8b4bf" + // Huffman-encoded value.
|
||||
"2703637573746f6d2d6b6579" + // Non-Huffman encoded name.
|
||||
"0c637573746f6d2d76616c7565" + // Non-Huffman encoded value.
|
||||
"2f0125a849e95ba97d7f" + // Huffman-encoded name.
|
||||
"0c637573746f6d2d76616c7565" + // Non-Huffman encoded value.
|
||||
"2703637573746f6d2d6b6579" + // Non-Huffman encoded name.
|
||||
"8925a849e95bb8e8b4bf", // Huffman-encoded value.
|
||||
),
|
||||
want: []header{
|
||||
{mayIndex, "custom-key", "custom-value"},
|
||||
{mayIndex, "custom-key", "custom-value"},
|
||||
{mayIndex, "custom-key", "custom-value"},
|
||||
{mayIndex, "custom-key", "custom-value"},
|
||||
},
|
||||
}, {
|
||||
name: "static table",
|
||||
enc: unhex("0000d1d45f00055452414345dfcc5f108621e9aec2a11f5c8294e75f1000"),
|
||||
want: []header{
|
||||
{mayIndex, ":method", "GET"},
|
||||
{mayIndex, ":method", "POST"},
|
||||
{mayIndex, ":method", "TRACE"},
|
||||
{mayIndex, "accept-encoding", "gzip, deflate, br"},
|
||||
{mayIndex, "location", ""},
|
||||
{mayIndex, "accept-encoding", "compress"},
|
||||
{mayIndex, "location", "foo"},
|
||||
{mayIndex, "accept-encoding", ""},
|
||||
},
|
||||
}} {
|
||||
runSynctestSubtest(t, test.name, func(t testing.TB) {
|
||||
st1, st2 := newStreamPair(t)
|
||||
st1.Write(test.enc)
|
||||
st1.Flush()
|
||||
|
||||
st2.lim = int64(len(test.enc))
|
||||
|
||||
var dec qpackDecoder
|
||||
got := []header{}
|
||||
err := dec.decode(st2, func(itype indexType, name, value string) error {
|
||||
got = append(got, header{itype, name, value})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("encoded: %x", test.enc)
|
||||
t.Errorf("got headers:")
|
||||
for _, h := range got {
|
||||
t.Errorf(" %v: %q", h.name, h.value)
|
||||
}
|
||||
t.Errorf("want headers:")
|
||||
for _, h := range test.want {
|
||||
t.Errorf(" %v: %q", h.name, h.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPACKDecodeErrors(t *testing.T) {
|
||||
// Many test cases here taken from Google QUICHE,
|
||||
// quiche/quic/core/qpack/qpack_encoder_test.cc.
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
enc []byte
|
||||
}{{
|
||||
name: "literal entry empty name",
|
||||
enc: unhex("00002003666f6f"),
|
||||
}, {
|
||||
name: "literal entry empty name and value",
|
||||
enc: unhex("00002000"),
|
||||
}, {
|
||||
name: "name length too large for varint",
|
||||
enc: unhex("000027ffffffffffffffffffff"),
|
||||
}, {
|
||||
name: "string literal too long",
|
||||
enc: unhex("000027ffff7f"),
|
||||
}, {
|
||||
name: "value length too large for varint",
|
||||
enc: unhex("000023666f6f7fffffffffffffffffffff"),
|
||||
}, {
|
||||
name: "value length too long",
|
||||
enc: unhex("000023666f6f7fffff7f"),
|
||||
}, {
|
||||
name: "incomplete header block",
|
||||
enc: unhex("00002366"),
|
||||
}, {
|
||||
name: "huffman name does not have eos prefix",
|
||||
enc: unhex("00002f0125a849e95ba97d7e8925a849e95bb8e8b4bf"),
|
||||
}, {
|
||||
name: "huffman value does not have eos prefix",
|
||||
enc: unhex("00002f0125a849e95ba97d7f8925a849e95bb8e8b4be"),
|
||||
}, {
|
||||
name: "huffman name eos prefix too long",
|
||||
enc: unhex("00002f0225a849e95ba97d7fff8925a849e95bb8e8b4bf"),
|
||||
}, {
|
||||
name: "huffman value eos prefix too long",
|
||||
enc: unhex("00002f0125a849e95ba97d7f8a25a849e95bb8e8b4bfff"),
|
||||
}, {
|
||||
name: "too high static table index",
|
||||
enc: unhex("0000ff23ff24"),
|
||||
}} {
|
||||
runSynctestSubtest(t, test.name, func(t testing.TB) {
|
||||
st1, st2 := newStreamPair(t)
|
||||
st1.Write(test.enc)
|
||||
st1.Flush()
|
||||
|
||||
st2.lim = int64(len(test.enc))
|
||||
|
||||
var dec qpackDecoder
|
||||
err := dec.decode(st2, func(itype indexType, name, value string) error {
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
t.Errorf("encoded: %x", test.enc)
|
||||
t.Fatalf("decode succeeded; want error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
47
internal/http3/qpack_encode.go
Normal file
47
internal/http3/qpack_encode.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.24
|
||||
|
||||
package http3
|
||||
|
||||
type qpackEncoder struct {
|
||||
// The encoder has no state for now,
|
||||
// but that'll change once we add dynamic table support.
|
||||
//
|
||||
// TODO: dynamic table support.
|
||||
}
|
||||
|
||||
func (qe *qpackEncoder) init() {
|
||||
staticTableOnce.Do(initStaticTableMaps)
|
||||
}
|
||||
|
||||
// encode encodes a list of headers into a QPACK encoded field section.
|
||||
//
|
||||
// The headers func must produce the same headers on repeated calls,
|
||||
// although the order may vary.
|
||||
func (qe *qpackEncoder) encode(headers func(func(itype indexType, name, value string))) []byte {
|
||||
// Encoded Field Section prefix.
|
||||
//
|
||||
// We don't yet use the dynamic table, so both values here are zero.
|
||||
var b []byte
|
||||
b = appendPrefixedInt(b, 0, 8, 0) // Required Insert Count
|
||||
b = appendPrefixedInt(b, 0, 7, 0) // Delta Base
|
||||
|
||||
headers(func(itype indexType, name, value string) {
|
||||
if itype == mayIndex {
|
||||
if i, ok := staticTableByNameValue[tableEntry{name, value}]; ok {
|
||||
b = appendIndexedFieldLine(b, staticTable, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
if i, ok := staticTableByName[name]; ok {
|
||||
b = appendLiteralFieldLineWithNameReference(b, staticTable, itype, i, value)
|
||||
} else {
|
||||
b = appendLiteralFieldLineWithLiteralName(b, itype, name, value)
|
||||
}
|
||||
})
|
||||
|
||||
return b
|
||||
}
|
||||
126
internal/http3/qpack_encode_test.go
Normal file
126
internal/http3/qpack_encode_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.24
|
||||
|
||||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQPACKEncode(t *testing.T) {
|
||||
type header struct {
|
||||
itype indexType
|
||||
name, value string
|
||||
}
|
||||
// Many test cases here taken from Google QUICHE,
|
||||
// quiche/quic/core/qpack/qpack_encoder_test.cc.
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
headers []header
|
||||
want []byte
|
||||
}{{
|
||||
name: "empty",
|
||||
headers: []header{},
|
||||
want: unhex("0000"),
|
||||
}, {
|
||||
name: "empty name",
|
||||
headers: []header{
|
||||
{mayIndex, "", "foo"},
|
||||
},
|
||||
want: unhex("0000208294e7"),
|
||||
}, {
|
||||
name: "empty value",
|
||||
headers: []header{
|
||||
{mayIndex, "foo", ""},
|
||||
},
|
||||
want: unhex("00002a94e700"),
|
||||
}, {
|
||||
name: "empty name and value",
|
||||
headers: []header{
|
||||
{mayIndex, "", ""},
|
||||
},
|
||||
want: unhex("00002000"),
|
||||
}, {
|
||||
name: "simple",
|
||||
headers: []header{
|
||||
{mayIndex, "foo", "bar"},
|
||||
},
|
||||
want: unhex("00002a94e703626172"),
|
||||
}, {
|
||||
name: "multiple",
|
||||
headers: []header{
|
||||
{mayIndex, "foo", "bar"},
|
||||
{mayIndex, "ZZZZZZZ", strings.Repeat("Z", 127)},
|
||||
},
|
||||
want: unhex("0000" + // prefix
|
||||
// foo: bar
|
||||
"2a94e703626172" +
|
||||
// 7 octet long header name, the smallest number
|
||||
// that does not fit on a 3-bit prefix.
|
||||
"27005a5a5a5a5a5a5a" +
|
||||
// 127 octet long header value, the smallest
|
||||
// number that does not fit on a 7-bit prefix.
|
||||
"7f005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +
|
||||
"5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +
|
||||
"5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" +
|
||||
"5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"),
|
||||
}, {
|
||||
name: "static table 1",
|
||||
headers: []header{
|
||||
{mayIndex, ":method", "GET"},
|
||||
{mayIndex, "accept-encoding", "gzip, deflate, br"},
|
||||
{mayIndex, "location", ""},
|
||||
},
|
||||
want: unhex("0000d1dfcc"),
|
||||
}, {
|
||||
name: "static table 2",
|
||||
headers: []header{
|
||||
{mayIndex, ":method", "POST"},
|
||||
{mayIndex, "accept-encoding", "compress"},
|
||||
{mayIndex, "location", "foo"},
|
||||
},
|
||||
want: unhex("0000d45f108621e9aec2a11f5c8294e7"),
|
||||
}, {
|
||||
name: "static table 3",
|
||||
headers: []header{
|
||||
{mayIndex, ":method", "TRACE"},
|
||||
{mayIndex, "accept-encoding", ""},
|
||||
},
|
||||
want: unhex("00005f000554524143455f1000"),
|
||||
}, {
|
||||
name: "never indexed literal field line with name reference",
|
||||
headers: []header{
|
||||
{neverIndex, ":method", ""},
|
||||
},
|
||||
want: unhex("00007f0000"),
|
||||
}, {
|
||||
name: "never indexed literal field line with literal name",
|
||||
headers: []header{
|
||||
{neverIndex, "a", "b"},
|
||||
},
|
||||
want: unhex("000031610162"),
|
||||
}} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var enc qpackEncoder
|
||||
enc.init()
|
||||
|
||||
got := enc.encode(func(f func(itype indexType, name, value string)) {
|
||||
for _, h := range test.headers {
|
||||
f(h.itype, h.name, h.value)
|
||||
}
|
||||
})
|
||||
if !bytes.Equal(got, test.want) {
|
||||
for _, h := range test.headers {
|
||||
t.Logf("header %v: %q", h.name, h.value)
|
||||
}
|
||||
t.Errorf("got: %x", got)
|
||||
t.Errorf("want: %x", test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
144
internal/http3/qpack_static.go
Normal file
144
internal/http3/qpack_static.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.24
|
||||
|
||||
package http3
|
||||
|
||||
import "sync"
|
||||
|
||||
type tableEntry struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
|
||||
// staticTableEntry returns the static table entry with the given index.
|
||||
func staticTableEntry(index int64) (tableEntry, error) {
|
||||
if index >= int64(len(staticTableEntries)) {
|
||||
return tableEntry{}, errQPACKDecompressionFailed
|
||||
}
|
||||
return staticTableEntries[index], nil
|
||||
}
|
||||
|
||||
func initStaticTableMaps() {
|
||||
staticTableByName = make(map[string]int)
|
||||
staticTableByNameValue = make(map[tableEntry]int)
|
||||
for i, ent := range staticTableEntries {
|
||||
if _, ok := staticTableByName[ent.name]; !ok {
|
||||
staticTableByName[ent.name] = i
|
||||
}
|
||||
staticTableByNameValue[ent] = i
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
staticTableOnce sync.Once
|
||||
staticTableByName map[string]int
|
||||
staticTableByNameValue map[tableEntry]int
|
||||
)
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9204.html#appendix-A
|
||||
//
|
||||
// Note that this is different from the HTTP/2 static table.
|
||||
var staticTableEntries = [...]tableEntry{
|
||||
0: {":authority", ""},
|
||||
1: {":path", "/"},
|
||||
2: {"age", "0"},
|
||||
3: {"content-disposition", ""},
|
||||
4: {"content-length", "0"},
|
||||
5: {"cookie", ""},
|
||||
6: {"date", ""},
|
||||
7: {"etag", ""},
|
||||
8: {"if-modified-since", ""},
|
||||
9: {"if-none-match", ""},
|
||||
10: {"last-modified", ""},
|
||||
11: {"link", ""},
|
||||
12: {"location", ""},
|
||||
13: {"referer", ""},
|
||||
14: {"set-cookie", ""},
|
||||
15: {":method", "CONNECT"},
|
||||
16: {":method", "DELETE"},
|
||||
17: {":method", "GET"},
|
||||
18: {":method", "HEAD"},
|
||||
19: {":method", "OPTIONS"},
|
||||
20: {":method", "POST"},
|
||||
21: {":method", "PUT"},
|
||||
22: {":scheme", "http"},
|
||||
23: {":scheme", "https"},
|
||||
24: {":status", "103"},
|
||||
25: {":status", "200"},
|
||||
26: {":status", "304"},
|
||||
27: {":status", "404"},
|
||||
28: {":status", "503"},
|
||||
29: {"accept", "*/*"},
|
||||
30: {"accept", "application/dns-message"},
|
||||
31: {"accept-encoding", "gzip, deflate, br"},
|
||||
32: {"accept-ranges", "bytes"},
|
||||
33: {"access-control-allow-headers", "cache-control"},
|
||||
34: {"access-control-allow-headers", "content-type"},
|
||||
35: {"access-control-allow-origin", "*"},
|
||||
36: {"cache-control", "max-age=0"},
|
||||
37: {"cache-control", "max-age=2592000"},
|
||||
38: {"cache-control", "max-age=604800"},
|
||||
39: {"cache-control", "no-cache"},
|
||||
40: {"cache-control", "no-store"},
|
||||
41: {"cache-control", "public, max-age=31536000"},
|
||||
42: {"content-encoding", "br"},
|
||||
43: {"content-encoding", "gzip"},
|
||||
44: {"content-type", "application/dns-message"},
|
||||
45: {"content-type", "application/javascript"},
|
||||
46: {"content-type", "application/json"},
|
||||
47: {"content-type", "application/x-www-form-urlencoded"},
|
||||
48: {"content-type", "image/gif"},
|
||||
49: {"content-type", "image/jpeg"},
|
||||
50: {"content-type", "image/png"},
|
||||
51: {"content-type", "text/css"},
|
||||
52: {"content-type", "text/html; charset=utf-8"},
|
||||
53: {"content-type", "text/plain"},
|
||||
54: {"content-type", "text/plain;charset=utf-8"},
|
||||
55: {"range", "bytes=0-"},
|
||||
56: {"strict-transport-security", "max-age=31536000"},
|
||||
57: {"strict-transport-security", "max-age=31536000; includesubdomains"},
|
||||
58: {"strict-transport-security", "max-age=31536000; includesubdomains; preload"},
|
||||
59: {"vary", "accept-encoding"},
|
||||
60: {"vary", "origin"},
|
||||
61: {"x-content-type-options", "nosniff"},
|
||||
62: {"x-xss-protection", "1; mode=block"},
|
||||
63: {":status", "100"},
|
||||
64: {":status", "204"},
|
||||
65: {":status", "206"},
|
||||
66: {":status", "302"},
|
||||
67: {":status", "400"},
|
||||
68: {":status", "403"},
|
||||
69: {":status", "421"},
|
||||
70: {":status", "425"},
|
||||
71: {":status", "500"},
|
||||
72: {"accept-language", ""},
|
||||
73: {"access-control-allow-credentials", "FALSE"},
|
||||
74: {"access-control-allow-credentials", "TRUE"},
|
||||
75: {"access-control-allow-headers", "*"},
|
||||
76: {"access-control-allow-methods", "get"},
|
||||
77: {"access-control-allow-methods", "get, post, options"},
|
||||
78: {"access-control-allow-methods", "options"},
|
||||
79: {"access-control-expose-headers", "content-length"},
|
||||
80: {"access-control-request-headers", "content-type"},
|
||||
81: {"access-control-request-method", "get"},
|
||||
82: {"access-control-request-method", "post"},
|
||||
83: {"alt-svc", "clear"},
|
||||
84: {"authorization", ""},
|
||||
85: {"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"},
|
||||
86: {"early-data", "1"},
|
||||
87: {"expect-ct", ""},
|
||||
88: {"forwarded", ""},
|
||||
89: {"if-range", ""},
|
||||
90: {"origin", ""},
|
||||
91: {"purpose", "prefetch"},
|
||||
92: {"server", ""},
|
||||
93: {"timing-allow-origin", "*"},
|
||||
94: {"upgrade-insecure-requests", "1"},
|
||||
95: {"user-agent", ""},
|
||||
96: {"x-forwarded-for", ""},
|
||||
97: {"x-frame-options", "deny"},
|
||||
98: {"x-frame-options", "sameorigin"},
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func TestStreamDiscardFrame(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func newStreamPair(t *testing.T) (s1, s2 *stream) {
|
||||
func newStreamPair(t testing.TB) (s1, s2 *stream) {
|
||||
t.Helper()
|
||||
q1, q2 := newQUICStreamPair(t)
|
||||
return newStream(q1), newStream(q2)
|
||||
|
||||
Reference in New Issue
Block a user