internal/parser/http2/frame.go

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