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