internal/parser/parser.go

// Package parser is the top-level dispatch between HTTP/1.1 and HTTP/2
// framing. The concrete wire-level decoders live in parser/http1 and
// parser/http2; this file decides which one to hand a byte stream to and
// keeps the per-connection state machine that the TUI ultimately observes.
//
// See mercemay.top/src/httptap/ for the published package docs.
package parser

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"sync"

	"mercemay.top/httptap/internal/parser/http1"
	"mercemay.top/httptap/internal/parser/http2"
)

// Direction labels a half of the TCP conversation.
type Direction uint8

const (
	DirClient Direction = iota
	DirServer
)

// Message is the shared shape produced by both protocol parsers.
type Message struct {
	Stream     uint32
	Direction  Direction
	Protocol   string
	StartLine  string
	Headers    [][2]string
	Body       []byte
	Trailers   [][2]string
	Incomplete bool
}

// Parser owns the per-connection state for a single byte direction.
type Parser struct {
	mu    sync.Mutex
	buf   *bufio.Reader
	mode  string
	h1    *http1.Decoder
	h2    *http2.Decoder
	sink  func(Message)
	close func()
}

// New returns a Parser that will read from r and call sink for each
// completed message. ALPN is sampled from the first handshake bytes if
// visible, otherwise we fall back to HTTP/1.1.
func New(r io.Reader, sink func(Message)) *Parser {
	return &Parser{
		buf:  bufio.NewReaderSize(r, 64*1024),
		sink: sink,
	}
}

// Run drives the parser until the underlying reader returns io.EOF.
// It is safe to call Close from another goroutine to interrupt.
func (p *Parser) Run() error {
	if err := p.sniff(); err != nil {
		return err
	}
	switch p.mode {
	case "h2":
		return p.h2.Run(p.sink)
	default:
		return p.h1.Run(p.sink)
	}
}

// sniff peeks the first 24 bytes and, if they match the HTTP/2 connection
// preface, switches to the binary framing decoder.
func (p *Parser) sniff() error {
	head, err := p.buf.Peek(24)
	if err != nil && !errors.Is(err, io.EOF) {
		return fmt.Errorf("parser: peek: %w", err)
	}
	if len(head) >= 24 && string(head) == "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" {
		if _, err := p.buf.Discard(24); err != nil {
			return err
		}
		p.mode = "h2"
		p.h2 = http2.NewDecoder(p.buf)
		return nil
	}
	p.mode = "h1"
	p.h1 = http1.NewDecoder(p.buf)
	return nil
}

// Close detaches the sink and unblocks Run if it is waiting on the reader.
func (p *Parser) Close() {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.close != nil {
		p.close()
		p.close = nil
	}
}

// Flush drains any buffered partial message as an incomplete Message so
// the UI can still render the last in-flight bytes on disconnect.
func (p *Parser) Flush() {
	p.mu.Lock()
	defer p.mu.Unlock()
	if p.h1 != nil {
		if m, ok := p.h1.Flush(); ok {
			m.Incomplete = true
			p.sink(Message(m))
		}
	}
	if p.h2 != nil {
		for _, m := range p.h2.Flush() {
			m.Incomplete = true
			p.sink(Message(m))
		}
	}
}