Skip to content

Commit

Permalink
internal/http3: QPACK encoding and decoding
Browse files Browse the repository at this point in the history
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 <[email protected]>
Auto-Submit: Damien Neil <[email protected]>
Reviewed-by: Jonathan Amsterdam <[email protected]>
Reviewed-by: Brad Fitzpatrick <[email protected]>
  • Loading branch information
neild authored and gopherbot committed Jan 23, 2025
1 parent 93c1957 commit c72e89d
Show file tree
Hide file tree
Showing 8 changed files with 795 additions and 2 deletions.
16 changes: 16 additions & 0 deletions internal/http3/http3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
package http3

import (
"encoding/hex"
"os"
"slices"
"strings"
"testing"
"testing/synctest"
)
Expand Down Expand Up @@ -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
}
183 changes: 182 additions & 1 deletion internal/http3/qpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
83 changes: 83 additions & 0 deletions internal/http3/qpack_decode.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit c72e89d

Please sign in to comment.