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:
Damien Neil
2025-01-08 16:01:09 -08:00
committed by Gopher Robot
parent 93c1957e42
commit c72e89d6a9
8 changed files with 795 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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.

View 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
}

View 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")
}
})
}
}

View 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
}

View 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)
}
})
}
}

View 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"},
}

View File

@@ -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)