mirror of
https://github.com/zap-proto/http.git
synced 2026-10-04 03:07:19 +00:00
Sixty error strings still opened with "zaphttp: " and the package comment still declared Package zaphttp. An error names the package it came from, so a reader grepping for the source of "zaphttp: truncated header block" would have found a package that no longer exists under that name. Co-authored-by: Hanzo Dev <[email protected]>
106 lines
3.8 KiB
Go
106 lines
3.8 KiB
Go
// Transport — length-prefixed framing over an io.ReadWriteCloser.
|
|
//
|
|
// v0.1 uses a 4-byte big-endian length prefix on each frame; v0.2
|
|
// will swap this for the full ZAP transport (X-Wing PQ KEM handshake
|
|
// + AEAD framing + multi-stream multiplexing) once zap-proto/go ships
|
|
// it. The marshal/unmarshal API in wire.go is stable across that
|
|
// change — only the bytes flowing on the socket change shape.
|
|
//
|
|
// Each ZAP-HTTP exchange is one request frame followed by one
|
|
// response frame on the same connection. Connections may be reused
|
|
// across exchanges (HTTP/1.1-style keep-alive); the transport itself
|
|
// does not pipeline.
|
|
|
|
package http
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// MaxFrameSize bounds an inbound frame. The wire format leaves room
|
|
// for larger frames; the limit here defends a server against a
|
|
// malicious peer announcing a multi-gigabyte length prefix.
|
|
const MaxFrameSize = 64 << 20 // 64 MiB
|
|
|
|
// readFrame reads one length-prefixed frame from r into a freshly allocated
|
|
// slice. nil + io.EOF indicates the peer closed cleanly between frames. Hot
|
|
// paths use readFrameInto to reuse a buffer instead.
|
|
func readFrame(r *bufio.Reader) ([]byte, error) {
|
|
return readFrameInto(r, nil)
|
|
}
|
|
|
|
// readFrameInto reads one length-prefixed frame into buf, growing it only when
|
|
// the frame exceeds its capacity, and returns the frame slice (buf reused). The
|
|
// caller reassigns its buffer from the return value so a grown buffer persists
|
|
// across calls — the steady-state read is allocation-free. The frame's bytes
|
|
// are consumed synchronously by Unmarshal{Request,Response} (which copy into the
|
|
// fasthttp store), so the buffer is free to reuse on the next call.
|
|
func readFrameInto(r *bufio.Reader, buf []byte) ([]byte, error) {
|
|
// Peek the length prefix out of the bufio buffer directly rather than
|
|
// io.ReadFull into a local [4]byte: passing a stack array through the
|
|
// io.Reader interface forces it to the heap on every call. Peek returns a
|
|
// view into the reader's own buffer — no copy, no escape.
|
|
hdr, err := r.Peek(4)
|
|
if err != nil {
|
|
return buf, err
|
|
}
|
|
n := binary.BigEndian.Uint32(hdr)
|
|
if _, err := r.Discard(4); err != nil {
|
|
return buf, err
|
|
}
|
|
if n == 0 {
|
|
return buf, fmt.Errorf("http: zero-length frame")
|
|
}
|
|
if n > MaxFrameSize {
|
|
return buf, fmt.Errorf("http: frame size %d exceeds MaxFrameSize=%d", n, MaxFrameSize)
|
|
}
|
|
if uint32(cap(buf)) < n {
|
|
buf = make([]byte, n)
|
|
} else {
|
|
buf = buf[:n]
|
|
}
|
|
// buf is already heap-backed (persistent across calls), so ReadFull's
|
|
// interface indirection costs no new allocation here.
|
|
if _, err := io.ReadFull(r, buf); err != nil {
|
|
return buf, fmt.Errorf("http: short frame: %w", err)
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
// writeFramePrefixed writes a length prefix + frame in ONE Write. The frame
|
|
// buffer must have been built with 4 leading bytes reserved for the prefix
|
|
// (frame[:4] is the placeholder); this halves the per-message syscall count vs
|
|
// writeFrame's separate header+body writes.
|
|
func writeFramePrefixed(w io.Writer, framePlusPrefix []byte) error {
|
|
body := len(framePlusPrefix) - 4
|
|
if body <= 0 {
|
|
return fmt.Errorf("http: refusing to write zero-length frame")
|
|
}
|
|
if uint64(body) > MaxFrameSize {
|
|
return fmt.Errorf("http: frame size %d exceeds MaxFrameSize=%d", body, MaxFrameSize)
|
|
}
|
|
binary.BigEndian.PutUint32(framePlusPrefix[0:4], uint32(body))
|
|
_, err := w.Write(framePlusPrefix)
|
|
return err
|
|
}
|
|
|
|
// writeFrame writes one length-prefixed frame to w.
|
|
func writeFrame(w io.Writer, frame []byte) error {
|
|
if len(frame) == 0 {
|
|
return fmt.Errorf("http: refusing to write zero-length frame")
|
|
}
|
|
if uint64(len(frame)) > MaxFrameSize {
|
|
return fmt.Errorf("http: frame size %d exceeds MaxFrameSize=%d", len(frame), MaxFrameSize)
|
|
}
|
|
var hdr [4]byte
|
|
binary.BigEndian.PutUint32(hdr[:], uint32(len(frame)))
|
|
if _, err := w.Write(hdr[:]); err != nil {
|
|
return err
|
|
}
|
|
_, err := w.Write(frame)
|
|
return err
|
|
}
|