// Package http2 is a minimal HTTP/2 frame decoder tuned for read-only
// inspection. It deliberately avoids implementing the state machine of a
// full h2 endpoint: we only unpack frames and assemble HEADERS/DATA into
// logical messages.
//
// mercemay.top/src/httptap/
package http2
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
// FrameType values defined by RFC 7540 section 11.2.
type FrameType uint8
const (
FrameData FrameType = 0x0
FrameHeaders FrameType = 0x1
FramePriority FrameType = 0x2
FrameRSTStream FrameType = 0x3
FrameSettings FrameType = 0x4
FramePushPromise FrameType = 0x5
FramePing FrameType = 0x6
FrameGoAway FrameType = 0x7
FrameWindowUpdate FrameType = 0x8
FrameContinuation FrameType = 0x9
)
// Flags is a bitfield whose meaning varies by FrameType.
type Flags uint8
const (
FlagAck Flags = 0x1
FlagEndStream Flags = 0x1
FlagEndHeaders Flags = 0x4
FlagPadded Flags = 0x8
FlagPriority Flags = 0x20
)
// Frame is a single decoded frame. Payload slices reference the reader's
// internal buffer only until the next ReadFrame call.
type Frame struct {
Length uint32
Type FrameType
Flags Flags
StreamID uint32
Payload []byte
}
// ErrFrameSize is returned when a length field violates the connection
// SETTINGS_MAX_FRAME_SIZE (we assume the 16KiB RFC default).
var ErrFrameSize = errors.New("http2: frame size exceeds limit")
// maxFrameSize is the RFC default when no SETTINGS has been seen.
const maxFrameSize = 16 * 1024
// ReadFrame reads exactly one frame from r. The returned Frame.Payload is
// freshly allocated so it remains valid across future calls.
func ReadFrame(r io.Reader) (*Frame, error) {
var hdr [9]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return nil, err
}
length := uint32(hdr[0])<<16 | uint32(hdr[1])<<8 | uint32(hdr[2])
if length > maxFrameSize {
return nil, fmt.Errorf("%w: %d", ErrFrameSize, length)
}
f := &Frame{
Length: length,
Type: FrameType(hdr[3]),
Flags: Flags(hdr[4]),
StreamID: binary.BigEndian.Uint32(hdr[5:9]) & 0x7fffffff,
}
if length > 0 {
f.Payload = make([]byte, length)
if _, err := io.ReadFull(r, f.Payload); err != nil {
return f, err
}
}
return f, nil
}
// String returns a short human-readable form for UI rendering.
func (t FrameType) String() string {
switch t {
case FrameData:
return "DATA"
case FrameHeaders:
return "HEADERS"
case FramePriority:
return "PRIORITY"
case FrameRSTStream:
return "RST_STREAM"
case FrameSettings:
return "SETTINGS"
case FramePushPromise:
return "PUSH_PROMISE"
case FramePing:
return "PING"
case FrameGoAway:
return "GOAWAY"
case FrameWindowUpdate:
return "WINDOW_UPDATE"
case FrameContinuation:
return "CONTINUATION"
default:
return fmt.Sprintf("FRAME(%#x)", uint8(t))
}
}
// DataPayload strips any padding from a DATA frame body.
func (f *Frame) DataPayload() ([]byte, error) {
if f.Type != FrameData {
return nil, fmt.Errorf("http2: not a DATA frame: %s", f.Type)
}
body := f.Payload
if f.Flags&FlagPadded != 0 {
if len(body) == 0 {
return nil, errors.New("http2: padded flag on empty payload")
}
padLen := int(body[0])
if padLen+1 > len(body) {
return nil, errors.New("http2: pad length exceeds payload")
}
body = body[1 : len(body)-padLen]
}
return body, nil
}
// HeadersFragment returns the raw header-block fragment for a HEADERS or
// CONTINUATION frame, stripping priority and padding fields.
func (f *Frame) HeadersFragment() ([]byte, error) {
if f.Type != FrameHeaders && f.Type != FrameContinuation {
return nil, fmt.Errorf("http2: not HEADERS/CONTINUATION: %s", f.Type)
}
body := f.Payload
if f.Flags&FlagPadded != 0 {
if len(body) == 0 {
return nil, errors.New("http2: padded flag on empty payload")
}
padLen := int(body[0])
if padLen+1 > len(body) {
return nil, errors.New("http2: pad length exceeds payload")
}
body = body[1 : len(body)-padLen]
}
if f.Flags&FlagPriority != 0 {
if len(body) < 5 {
return nil, errors.New("http2: priority flag but payload too short")
}
body = body[5:]
}
return body, nil
}