1 Commits

Author SHA1 Message Date
Patricio Whittingslow 9758d48696 clanker tls 2026-07-24 20:56:52 -03:00
56 changed files with 2997 additions and 8058 deletions
-1
View File
@@ -16,7 +16,6 @@ Userspace networking primitives.
- Zero scheduling required. No goroutines/channels use in Lneto. Can be run in event loop.
- Heapless packet processing
- [`httpraw`](https://github.com/soypat/lneto/tree/main/http/httpraw) is likely the most performant HTTP/1.1 processing package in the Go ecosystem. Based on [`fasthttp`](https://github.com/valyala/fasthttp) but simpler and more thoughtful memory use.
- [`httφ`](https://github.com/soypat/lneto/tree/main/http/httphi) - Heapless HTTP router and zero-copy response writing with Go's standard library API.
- Lean memory footprint
- HTTP header struct is 80 bytes with no runtime usage nor heap usage other than buffer
- Entire Ethernet+IPv4+UDP+DHCP+DNS+NTP stack in ~2kB RAM.
+4 -4
View File
@@ -17,9 +17,9 @@ const _Operation_name = "requestreply"
var _Operation_index = [...]uint8{0, 7, 12}
func (i Operation) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_Operation_index)-1 {
return "Operation(" + strconv.FormatInt(int64(i), 10) + ")"
i -= 1
if i >= Operation(len(_Operation_index)-1) {
return "Operation(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _Operation_name[_Operation_index[idx]:_Operation_index[idx+1]]
return _Operation_name[_Operation_index[i]:_Operation_index[i+1]]
}
-3
View File
@@ -44,9 +44,6 @@ type Frame struct {
buf []byte
}
// RawData returns the underlying frame buffer.
func (frm Frame) RawData() []byte { return frm.buf }
// OptionsPayload returns the options portion of the DHCP frame. May be zero lengthed.
func (frm Frame) OptionsPayload() []byte {
return frm.buf[OptionsOffset:]
+8 -10
View File
@@ -106,11 +106,10 @@ const _Op_name = "undefinedrequestreply"
var _Op_index = [...]uint8{0, 9, 16, 21}
func (i Op) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Op_index)-1 {
if i >= Op(len(_Op_index)-1) {
return "Op(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Op_name[_Op_index[idx]:_Op_index[idx+1]]
return _Op_name[_Op_index[i]:_Op_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -132,11 +131,10 @@ const _MessageType_name = "undefineddiscoverofferrequestdeclineacknakreleaseinfo
var _MessageType_index = [...]uint8{0, 9, 17, 22, 29, 36, 39, 42, 49, 55}
func (i MessageType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_MessageType_index)-1 {
if i >= MessageType(len(_MessageType_index)-1) {
return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _MessageType_name[_MessageType_index[idx]:_MessageType_index[idx+1]]
return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -157,9 +155,9 @@ const _ClientState_name = "initselectingrequestingboundrenewingrebindinginit-reb
var _ClientState_index = [...]uint8{0, 4, 13, 23, 28, 36, 45, 56, 65}
func (i ClientState) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_ClientState_index)-1 {
return "ClientState(" + strconv.FormatInt(int64(i), 10) + ")"
i -= 1
if i >= ClientState(len(_ClientState_index)-1) {
return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _ClientState_name[_ClientState_index[idx]:_ClientState_index[idx+1]]
return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]]
}
+14 -15
View File
@@ -28,11 +28,11 @@ const _MsgType_name = "solicitadvertiserequestconfirmrenewrebindreplyreleasedecl
var _MsgType_index = [...]uint8{0, 7, 16, 23, 30, 35, 41, 46, 53, 60, 71, 85, 95, 105}
func (i MsgType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_MsgType_index)-1 {
return "MsgType(" + strconv.FormatInt(int64(i), 10) + ")"
i -= 1
if i >= MsgType(len(_MsgType_index)-1) {
return "MsgType(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _MsgType_name[_MsgType_index[idx]:_MsgType_index[idx+1]]
return _MsgType_name[_MsgType_index[i]:_MsgType_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -51,11 +51,11 @@ const _ClientState_name = "initsolicitingrequestingboundrenewingrebinding"
var _ClientState_index = [...]uint8{0, 4, 14, 24, 29, 37, 46}
func (i ClientState) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_ClientState_index)-1 {
return "ClientState(" + strconv.FormatInt(int64(i), 10) + ")"
i -= 1
if i >= ClientState(len(_ClientState_index)-1) {
return "ClientState(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _ClientState_name[_ClientState_index[idx]:_ClientState_index[idx+1]]
return _ClientState_name[_ClientState_index[i]:_ClientState_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -134,11 +134,10 @@ const _StatusCode_name = "successunspec-failno-addrs-availno-bindingnot-on-linku
var _StatusCode_index = [...]uint8{0, 7, 18, 32, 42, 53, 66}
func (i StatusCode) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_StatusCode_index)-1 {
if i >= StatusCode(len(_StatusCode_index)-1) {
return "StatusCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _StatusCode_name[_StatusCode_index[idx]:_StatusCode_index[idx+1]]
return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -154,9 +153,9 @@ const _DUIDType_name = "duid-lltduid-enduid-ll"
var _DUIDType_index = [...]uint8{0, 8, 15, 22}
func (i DUIDType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_DUIDType_index)-1 {
return "DUIDType(" + strconv.FormatInt(int64(i), 10) + ")"
i -= 1
if i >= DUIDType(len(_DUIDType_index)-1) {
return "DUIDType(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _DUIDType_name[_DUIDType_index[idx]:_DUIDType_index[idx+1]]
return _DUIDType_name[_DUIDType_index[i]:_DUIDType_index[i+1]]
}
+4 -6
View File
@@ -119,11 +119,10 @@ const _RCode_name = "successformat errorserver failurename errornot implementedr
var _RCode_index = [...]uint8{0, 7, 19, 33, 43, 58, 65}
func (i RCode) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RCode_index)-1 {
if i >= RCode(len(_RCode_index)-1) {
return "RCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RCode_name[_RCode_index[idx]:_RCode_index[idx+1]]
return _RCode_name[_RCode_index[i]:_RCode_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -139,9 +138,8 @@ const _OpCode_name = "Standard queryInverse queryServer status request"
var _OpCode_index = [...]uint8{0, 14, 27, 48}
func (i OpCode) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_OpCode_index)-1 {
if i >= OpCode(len(_OpCode_index)-1) {
return "OpCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _OpCode_name[_OpCode_index[idx]:_OpCode_index[idx+1]]
return _OpCode_name[_OpCode_index[i]:_OpCode_index[i+1]]
}
@@ -288,7 +288,7 @@ func handleConnNet(conn net.Conn) error {
}
}
method := string(hdr.Method())
uri := string(hdr.RequestTarget())
uri := string(hdr.RequestURI())
fmt.Printf("< %s %s\n", method, uri)
var resp httpraw.Header
@@ -370,7 +370,7 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) {
var hdr httpraw.Header
hdr.SetMethod("GET")
hdr.SetRequestTarget("/")
hdr.SetRequestURI("/")
hdr.SetProtocol("HTTP/1.1")
hdr.Set("Host", string(ipv4.AppendFormatAddr(nil, stack.Addr4())))
hdr.Set("User-Agent", "lneto-mock")
+96
View File
@@ -0,0 +1,96 @@
//go:build !tinygo && linux
package main
import (
"net/netip"
"syscall"
)
// Conn wraps an accepted TCP connection from a raw Linux socket file descriptor.
// It implements io.Reader/io.Writer/io.Closer over syscall.Read/Write/Close.
type Conn struct {
fd int
remote netip.AddrPort
}
// Read reads bytes from the connection into b.
func (c *Conn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
n, err := syscall.Read(c.fd, b)
if err != nil {
return 0, err
}
if n == 0 {
return 0, syscall.ECONNRESET // Peer closed.
}
return n, nil
}
// Write writes b to the connection, looping until all bytes are sent.
func (c *Conn) Write(b []byte) (int, error) {
total := 0
for total < len(b) {
n, err := syscall.Write(c.fd, b[total:])
if err != nil {
return total, err
}
total += n
}
return total, nil
}
// Close closes the underlying file descriptor.
func (c *Conn) Close() error {
return syscall.Close(c.fd)
}
// RemoteAddr returns the peer address of the connection.
func (c *Conn) RemoteAddr() netip.AddrPort { return c.remote }
// Listener wraps a listening TCP socket bound to a local port.
type Listener struct {
fd int
}
// Listen creates a listening TCP socket bound to port on all interfaces.
func Listen(port uint16) (*Listener, error) {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil {
return nil, err
}
// Allow quick rebind after restart.
if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
syscall.Close(fd)
return nil, err
}
addr := &syscall.SockaddrInet4{Port: int(port)}
if err = syscall.Bind(fd, addr); err != nil {
syscall.Close(fd)
return nil, err
}
if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
syscall.Close(fd)
return nil, err
}
return &Listener{fd: fd}, nil
}
// Accept blocks until an incoming connection arrives and returns it as a Conn.
func (l *Listener) Accept(conn *Conn) error {
nfd, sa, err := syscall.Accept(l.fd)
if err != nil {
return err
}
conn.fd = nfd
if sa4, ok := sa.(*syscall.SockaddrInet4); ok {
conn.remote = netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port))
}
return nil
}
// Close closes the listening socket.
func (l *Listener) Close() error { return syscall.Close(l.fd) }
+61 -60
View File
@@ -3,77 +3,42 @@
package main
import (
"log/slog"
"net"
"os"
"strconv"
"sync/atomic"
"time"
"github.com/soypat/lneto/http/httphi"
"github.com/soypat/lneto/http/httpraw"
)
const (
kB = 1 << 10
listenPort = 8080
bufferSizes = 2 * kB
// A browser sends around twenty header fields; a request carrying more
// than this is answered 431 rather than parsed into memory it was not
// given. Each field costs 8 bytes of table.
numHeaderFields = 32
numGoroutines = 4
readTimeout = 2 * time.Second
)
const listenPort = 8080
func main() {
if err := run(); err != nil {
println("Error:", err.Error())
println("Error: ", err)
os.Exit(1)
}
println("DONE")
}
func run() error {
ln, err := net.Listen("tcp", ":"+strconv.Itoa(listenPort))
ln, err := Listen(listenPort)
if err != nil {
return err
}
defer ln.Close()
print("listening on http://localhost:", listenPort, "\n")
var mux httphi.MuxSlice
mux.Handle("GET /", homepage)
var router httphi.Router
err = router.Configure(httphi.RouterConfig{
FixedNumGoroutines: numGoroutines,
RequestHeaderBufferSize: bufferSizes,
RequestNumHeaderKVCap: numHeaderFields,
ResponseHeaderMinBufferSize: bufferSizes,
MaxAwaitingConns: 256,
Mux: &mux,
Logger: slog.Default(),
})
if err != nil {
return err
}
defer router.Shutdown()
println("listening on port", listenPort)
conn := new(Conn)
for {
conn, err := ln.Accept()
err := ln.Accept(conn)
if err != nil {
return err
}
// The connection owns the idle policy: a peer that opens a socket and
// then stalls fails its read instead of holding a router goroutine.
conn.SetReadDeadline(time.Now().Add(readTimeout))
err = router.Handle(conn)
if err != nil {
// Every goroutine is busy and the queue is full. Dropping the
// connection is the backpressure: memory stays bounded.
slog.Warn("dropped connection", slog.String("remote", conn.RemoteAddr().String()), slog.String("err", err.Error()))
conn.Close()
visits.Add(1)
if err := handle(conn); err != nil {
println("handle:", conn.RemoteAddr().String(), err.Error())
}
conn.Close()
}
}
@@ -85,22 +50,58 @@ const (
htmlTail = `!</blink></h1>` +
`<font color="#FF00FF">Sign my guestbook!</font>` +
`<br><hr>Best viewed in Netscape Navigator</center></body></html>`
// maxPage bounds the rendered page: both halves plus the visitor number.
maxPage = len(htmlHead) + 20 + len(htmlTail)
)
// visits counts served requests. Handlers run on the router's goroutines, so
// every visitor gets their own number.
var visits atomic.Uint64
const maxHTTPHeader = 1024
func homepage(exch *httphi.Exchange) {
var page [maxPage]byte
n := copy(page[:], htmlHead)
n += len(strconv.AppendUint(page[n:n], visits.Add(1), 10))
n += copy(page[n:], htmlTail)
var (
hdr httpraw.Header
httpbuf [maxHTTPHeader]byte
htmlbuf [512]byte
visits atomic.Uint64
)
exch.StageHeader("Content-Type", "text/html")
exch.StageHeaderInt("Content-Length", int64(n))
exch.WriteHeader(int(httphi.StatusOK))
exch.WriteBody(page[:n])
func handle(conn *Conn) error {
hdr.Reset(httpbuf[:0])
hdr.EnableBufferGrowth(false) // Limit memory to buffer capacity.
const incomingIsResponse = false // We get HTTP requests from clients.
deadline := time.Now().Add(50 * time.Millisecond)
for time.Until(deadline) > 0 {
if _, err := hdr.ReadFromLimited(conn, maxHTTPHeader); err != nil {
return err
}
needmoredata, err := hdr.TryParse(incomingIsResponse)
if err != nil {
return err
}
if needmoredata {
continue
}
break
}
println("\n\n================\n\n", hdr.String())
if time.Since(deadline) > 0 {
print("DEADLINE EXCEED: ", hdr.BufferParsed(), "/", hdr.BufferReceived(), " bytes parsed/read\n")
return nil
}
// Prepare tacky HTML response.
n := copy(htmlbuf[:], htmlHead)
n += len(strconv.AppendUint(htmlbuf[n:n], visits.Load(), 10))
n += copy(htmlbuf[n:], htmlTail)
contentLen := n
hdr.Reset(httpbuf[:0])
hdr.SetProtocol("HTTP/1.1")
hdr.SetStatus("200", "OK")
hdr.Set("Content-Type", "text/html")
hdr.SetInt("Content-Length", int64(contentLen), 10)
// Here we do some buffer juggling. We use remaining space
// of HTTP Header buffer to write the response that will be written over the wire.
respbuf := httpbuf[hdr.BufferUsed():]
header, err := hdr.AppendResponse(respbuf[:0])
if err != nil {
return err
}
conn.Write(header)
_, err = conn.Write(htmlbuf[:contentLen])
return err
}
+2 -2
View File
@@ -27,7 +27,7 @@ func run() error {
// Prepare GET request.
var hdr httpraw.Header
hdr.SetMethod("GET")
hdr.SetRequestTarget("/")
hdr.SetRequestURI("/")
hdr.SetProtocol("HTTP/1.1")
req, err := hdr.AppendRequest(nil)
if err != nil {
@@ -50,7 +50,7 @@ func run() error {
}()
fmt.Println(time.Now().Format("15:04:05.000"), "writing...")
conn.Write(req)
hdr.Reset(nil, 0) // No preallocation. Both key/value and raw buffer will grow and allocate.
hdr.Reset(nil)
var needMore bool = true
for needMore {
_, err = hdr.ReadFromLimited(conn, 1024)
+1 -1
View File
@@ -282,7 +282,7 @@ func handleConnection(conn *tcp.Conn) error {
}
method := string(hdr.Method())
uri := string(hdr.RequestTarget())
uri := string(hdr.RequestURI())
fmt.Printf("< %s %s\n", method, uri)
// Build response body.
+1 -1
View File
@@ -307,7 +307,7 @@ func run() (err error) {
timeHTTPCreate := timer("create HTTP GET request")
var hdr httpraw.Header
hdr.SetMethod("GET")
hdr.SetRequestTarget("/")
hdr.SetRequestURI("/")
hdr.SetProtocol("HTTP/1.1")
hdr.Set("Host", flagHostToResolve)
hdr.Set("User-Agent", "lneto")
-55
View File
@@ -1,55 +0,0 @@
# httφ
Heapless HTTP/1.1 router with `net/http`-shaped handlers. Benchmarked against `net/http` and friends at [**soypat/httpbench**](https://github.com/soypat/httpbench).
## Why
`net/http` allocates per request: `Request`, header map, response buffer, goroutine. Fine on a
server, fatal on a microcontroller. httφ pays that cost once, at `Router.Configure`:
- Exchanges and goroutines are fixed there. Serving allocates nothing; memory does not grow with load.
- No free exchange means `Handle` refuses the connection unclosed, rather than allocating one more of everything.
- `Handle` takes an `io.ReadWriteCloser`, so the router runs over a raw socket, an [`lneto`](https://github.com/soypat/lneto) TCP stack or a test pipe. No listener, no OS, no clock.
Parsing is [`httpraw`](../httpraw).
## Example
```go
var mux httphi.MuxSlice
mux.Handle("GET /", func(ex *httphi.Exchange) {
ex.WriteBody([]byte("hello world"))
})
var router httphi.Router
err := router.Configure(httphi.RouterConfig{
FixedNumGoroutines: 4, // 4 workers, 4 exchanges, allocated here and never again.
MaxAwaitingConns: 8, // Queue depth. Full queue drops connections.
RequestHeaderBufferSize: 1024,
ResponseHeaderMinBufferSize: 32, // Shares the request buffer.
RequestNumHeaderKVCap: 32,
Mux: &mux,
})
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept() // Accepting is the caller's job.
if err != nil {
log.Fatal(err)
}
if err = router.Handle(conn); err != nil {
conn.Close() // Refused: never blocks, never queues unboundedly.
}
}
```
`FixedNumGoroutines: -1` gives the unbounded flavor, a goroutine and an exchange per connection.
Runnable server over raw Linux sockets, plus query, form and multipart handlers:
[`example_test.go`](./example_test.go).
## Naming
Gonna be honest with y'all. I initially wanted it to be named `httplo` until I saw I could write `httphi.MethHead` with a small change.
-137
View File
@@ -1,137 +0,0 @@
package httphi
import (
"io"
"testing"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal"
)
// benchConn replays a fixed request and discards the response. It allocates
// nothing itself so benchmark alloc counts belong to the package under test.
type benchConn struct {
request string
read int
written int
}
func (c *benchConn) rewind() { c.read, c.written = 0, 0 }
func (c *benchConn) Read(b []byte) (int, error) {
if c.read >= len(c.request) {
return 0, io.EOF
}
n := copy(b, c.request[c.read:])
c.read += n
return n, nil
}
func (c *benchConn) Write(b []byte) (int, error) {
c.written += len(b)
return len(b), nil
}
func (c *benchConn) Close() error { return nil }
// benchBody is package level: converting a string literal to []byte inside the
// handler would allocate on every request and hide the router's own cost.
var benchBody = []byte("hello world")
func benchExchange(b *testing.B, conn conn) *Exchange {
b.Helper()
const bufferSize = 1024
const numHeaderCap = 2
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, 2*bufferSize),
RequestBufferLim: bufferSize,
NumHeaderKVCap: numHeaderCap,
})
if !exch.Acquire(conn) {
b.Fatal("fresh exchange failed to acquire connection")
}
return exch
}
// BenchmarkHandle measures a whole exchange: read, parse, mux and respond.
func BenchmarkHandle(b *testing.B) {
expect := []byte("123")
buf := make([]byte, 64)
for _, bb := range []struct {
name string
request string
handler HandlerFunc
}{
{
name: "GETWithHeadersAndQuery",
request: "GET /?abc=123 HTTP/1.1\r\nHost: tinygo.org\r\nUser-Agent: bench\r\nAccept: */*\r\nConnection: close\r\n\r\n",
handler: func(ex *Exchange) {
ex.StageHeader("Content-Type", "text/plain")
ex.StageHeaderIntBase("Content-Length", int64(len(benchBody)), 10)
data, present := ex.RequestQueryAppend(buf[:0], "abc", true)
if !present || !internal.BytesEqual(data, expect) {
panic("invalid result")
}
ex.WriteBody(benchBody)
},
},
{
name: "NotFound",
request: "GET /nowhere HTTP/1.1\r\nHost: tinygo.org\r\n\r\n",
handler: nil, // Unregistered: exercises the 404 path.
},
} {
b.Run(bb.name, func(b *testing.B) {
var mux MuxSlice
if bb.handler != nil {
mux.Handle("GET /", bb.handler)
}
conn := &benchConn{request: bb.request}
exch := benchExchange(b, conn)
b.ReportAllocs()
b.SetBytes(int64(len(bb.request)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
conn.rewind()
exch.Release()
exch.Acquire(conn)
Handle(exch, &mux, nopBackoff)
}
})
}
}
// benchForm is package level so the Form's pair slice is reused across requests,
// as a real handler holding one per goroutine would.
var benchForm httpraw.Form
// BenchmarkRequestParseForm measures reading and parsing a urlencoded body into
// a buffer the caller owns. Nothing on the path may allocate.
func BenchmarkRequestParseForm(b *testing.B) {
const request = "POST /f HTTP/1.1\r\nHost: tinygo.org\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\nContent-Length: 27\r\n\r\n" +
"user=gopher&msg=hello+world"
// The form owns its memory now, so pre-size it and forbid growth: an
// allocation on this path is the failure the benchmark is watching for.
benchForm.Reset(make([]byte, 0, 64), 2)
benchForm.EnableBufferGrowth(false)
var mux MuxSlice
mux.Handle("POST /f", func(ex *Exchange) {
err := ex.RequestParseForm(&benchForm, false, false)
if err != nil || benchForm.Len() != 2 {
panic("invalid result")
}
})
conn := &benchConn{request: request}
exch := benchExchange(b, conn)
b.ReportAllocs()
b.SetBytes(int64(len(request)))
b.ResetTimer()
for b.Loop() {
conn.rewind()
exch.Release()
exch.Acquire(conn)
Handle(exch, &mux, nopBackoff)
}
}
-112
View File
@@ -1,112 +0,0 @@
package httphi_test
import (
"fmt"
"io"
"log"
"log/slog"
"net"
"os"
"github.com/soypat/lneto/http/httphi"
"github.com/soypat/lneto/http/httpraw"
)
// ExampleRouter_linux goes over how to setup a linux server using raw linux connections.
// See [ExampleMuxSlice_query_forms_multipart] on how to define handlers for common HTTP processing.
func ExampleRouter() {
// Chrome tends to send ~700 bytes on a typical landing page request.
const requestBuffer = 1024
const numHeaderKV = requestBuffer / 32 //
var mux httphi.MuxSlice
mux.Handle("GET /", func(ex *httphi.Exchange) {
ex.WriteBody([]byte("hello world"))
})
var router httphi.Router
err := router.Configure(httphi.RouterConfig{
FixedNumGoroutines: -1, // Unbounded goroutines and allocations.
RequestHeaderBufferSize: requestBuffer,
ResponseHeaderMinBufferSize: 32, // Shared buffer with Request, not strictly necessary, especially if not sending headers.
RequestNumHeaderKVCap: numHeaderKV,
NormalizeOutgoingKeys: true,
Mux: &mux,
Logger: slog.Default(),
})
if err != nil {
log.Fatal(err)
}
const port = ":8080"
listener, err := net.Listen("tcp", port)
if err != nil {
log.Fatal(err)
}
log.Printf("server up at http://localhost%s", port)
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
err = router.Handle(conn)
if err != nil {
log.Println("httphi.Router failed to handle connection:", err)
}
}
}
// ExampleMuxSlice_query_forms_multipart goes over how to define Handlers (HandleFunc).
// See [ExampleRouter_linux] for how to setup the server.
func ExampleMuxSlice_query_forms_multipart() {
var mux httphi.MuxSlice
mux.Handle("/users/{id}", func(ex *httphi.Exchange) {
userID := ex.PathValue("id")
fmt.Printf("someone requested data for user %s\n", userID)
})
mux.Handle("/query", func(ex *httphi.Exchange) {
// query parameter in URL.
const decodeQuery = true
const queryKey = "search"
valueRaw, present := ex.RequestQueryValue(queryKey)
if !present {
return
}
valueDecoded, present := ex.RequestQueryAppend(nil, queryKey, decodeQuery)
fmt.Printf("got query=%v %s=%s (raw:%s)\n", present, queryKey, valueDecoded, valueRaw)
})
mux.Handle("GET /form", func(ex *httphi.Exchange) {
// Request Body Form. The form owns the memory: hand it a buffer and
// forbid growth to bound what a request may spend.
var form httpraw.Form
form.Reset(make([]byte, 0, 1024), 8) // Room for 8 pairs.
form.EnableBufferGrowth(false)
err := ex.RequestParseForm(&form, false, false)
if err != nil {
ex.WriteHeader(httphi.StatusInternalServerError)
return
}
for i := range form.Len() {
k, v := form.Pair(i)
fmt.Printf("received form value %d: %s=%s\n", i, k, v)
}
})
mux.Handle("GET /file-upload", func(ex *httphi.Exchange) {
// File upload directly onto server using multipart.
formbuf := make([]byte, 1024)
_, err := ex.ReadMultiparts(nil, formbuf, func(hdr *httpraw.MultipartHeader) io.WriteCloser {
fp, err := os.Create(string(hdr.Filename))
if err != nil {
return nil
}
return fp // Will write full file contents to fp.
})
if err != nil {
ex.WriteHeader(httphi.StatusInternalServerError)
return
}
})
}
-845
View File
@@ -1,845 +0,0 @@
package httphi
import (
"io"
"net"
"slices"
"strconv"
"sync/atomic"
"unsafe"
"github.com/soypat/lneto"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal"
)
// maxStatusLine bounds the response status line: "HTTP/1.1 " + 3 digit code +
// " " + longest [StatusText] + CRLF.
const maxStatusLine = len("HTTP/1.1 ") + 3 + 1 + len("Network Authentication Required") + 2
// Exchange is a single request-response cycle over a connection, playing the
// part of both http.Request and http.ResponseWriter: Request* methods read the
// request, [Exchange.StageHeader] and [Exchange.WriteBody] produce the response.
// A [Router] owns a fixed pool of them, which is what bounds its memory.
//
// Request and response share one buffer, the response header being written over
// the bytes that follow the parsed request header. Read the request body with
// [Exchange.ReadBody] before setting response headers.
type Exchange struct {
acquired atomic.Bool
gen atomic.Uint32
respTopBuf [maxStatusLine]byte
respTopWritten uint8
rawbuf []byte
respHeaderOff uint16
respHeaderLen uint16
reqHdr httpraw.Header
pathValues []PathValue
// bodyRW is the reader handed to [httpraw.Form.ReadLimited], kept here so
// boxing it into an io.Reader allocates nothing per request.
bodyRW ExchangeRW
hijacked bool
rw conn
matchedPattern string
respRemains int
respErr error // Sticky: response is unrecoverable once a write fails.
headerWritten bool
normalizeKeys bool
nextFree *Exchange
readErr error
}
// ExchangeConfig is the memory an [Exchange] is fixed to for the rest of its
// life by [Exchange.Configure]. A [Router] derives one per exchange from its
// [RouterConfig], which is what bounds the router's memory.
//
// Fields open with Required, Conditional or Optional and the constraint in
// brackets, as in [RouterConfig].
type ExchangeConfig struct {
// Required [non-empty] single buffer holding the request header, the response
// header and any surplus body. See [Exchange.UnsafeRawBuffer].
RawBuf []byte
// Required [<=len(RawBuf)] bytes of RawBuf reserved for the request header,
// the rest being the response. Configure panics if it exceeds RawBuf.
RequestBufferLim int
// Required [>0] request header fields that may be parsed. A request carrying
// more is answered 431, see [httpraw.ErrHeaderTooMany].
NumHeaderKVCap int
// Optional [any] normalization of staged response header keys as they are
// written, i.e: "content-type" becomes "Content-Type".
NormalizeOutgoingKeys bool
// Optional [any] cap holding the request header to RequestBufferLim rather than
// growing it. A header outgrowing it is answered 431, see [httpraw.ErrBufferExhausted].
NoRequestBufferGrowth bool
// Conditional [>=the most wildcards any one registered pattern binds] number of
// path values bindable, read back with [Exchange.PathValue]. A pattern binding
// more never matches, see [SetPathValues]. Zero suits a mux of literal patterns.
MaxPathValues int
}
// HijackRaw is a low-level implementation of http.Hijacker interface.
// A Hijack method is not exposed due to heap allocation implications and correctness concerns.
// Below is what an actual implementation may look like:
//
// func (exch *Exchange) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// conn, ok := exch.rw.(net.Conn)
// if !ok {
// return nil, nil, errors.New("net.Conn not implemented")
// }
// _, data, err := exch.HijackRaw(nil)
// if err != nil {
// return nil, nil, err
// }
// var rd *bufio.ReadWriter
// if len(data) > 0 {
// rd = &bufio.ReadWriter{Reader: bufio.NewReader(bytes.NewReader(data))}
// }
// return conn, rd, nil
// }
func (exch *Exchange) HijackRaw(dstBody []byte) (conn, []byte, error) {
data, err := exch.remainingSurplusBody()
if err != nil {
return nil, nil, err
}
exch.hijacked = true
dstBody = append(dstBody, data...)
return exch.rw, dstBody, nil
}
// Configure sets the memory the exchange works with for the rest of its life:
// rawbuf holds the request header, the response header and any surplus body,
// of which the first requestLim bytes are reserved for the request header.
// Panics if requestLim exceeds the buffer. Set normalizeKeys to normalize
// outgoing header keys, i.e: "content-type" to "Content-Type".
func (exch *Exchange) Configure(cfg ExchangeConfig) {
respSize := len(cfg.RawBuf) - cfg.RequestBufferLim
if respSize < 0 {
panic("request lim larger than buffer")
}
exch.rawbuf = cfg.RawBuf
exch.reqHdr.Reset(cfg.RawBuf[:0:cfg.RequestBufferLim], cfg.NumHeaderKVCap)
exch.reqHdr.ConfigBufferGrowth(!cfg.NoRequestBufferGrowth)
exch.normalizeKeys = cfg.NormalizeOutgoingKeys
internal.SliceReuse(&exch.pathValues, cfg.MaxPathValues)
exch.pathValues = exch.pathValues[:cfg.MaxPathValues]
}
// Acquire claims the exchange for conn and resets it to serve a new request,
// reusing the buffer set by [Exchange.Configure]. Returns false if the exchange
// is already serving, in which case conn is untouched.
func (exch *Exchange) Acquire(conn conn) bool {
if !exch.acquired.CompareAndSwap(false, true) {
return false
}
exch.matchedPattern = ""
exch.gen.Add(1)
exch.readErr = nil
exch.respErr = nil
exch.hijacked = false
exch.respTopWritten = 0
exch.respHeaderOff = 0
exch.respHeaderLen = 0
exch.respRemains = 0
exch.rw = conn
exch.headerWritten = false
exch.nextFree = nil
clear(exch.pathValues)
exch.reqHdr.Reset(nil, 0)
return true
}
// Release closes the exchange's connection and frees the exchange for a future
// [Exchange.Acquire]. The connection is left open if the handler took ownership
// of it with [Exchange.HijackRaw].
func (exch *Exchange) Release() {
if !exch.hijacked {
exch.rw.Close()
}
exch.rw = nil
exch.gen.Add(1)
exch.acquired.Store(false)
}
// UnsafeRawBuffer returns the contiguous buffer owned by [Exchange] being used for the request and response.
//
// Writing to it will mangle the entire request header+body and/or any staged response headers.
// Does not return the buffer used for the response first line so can be safely
// written to and used without modifying the staged response first line.
//
// Staging headers will write to this buffer so use mindfully.
// To access only the request header buffer portion use [httpraw.Header.BufferRaw] limited
// to [httpraw.Header.BufferParsed] as returned by [Exchange.RequestHeaderRaw].
// Writing to this section will not change the contents read by [Exchange.ReadBody].
//
// In [Router] context, the size of this buffer is influenced directly by [RouterConfig] HeaderBufferSize fields.
func (exch *Exchange) UnsafeRawBuffer() []byte { return exch.rawbuf }
// StageHeader stages a response header field, written on the first
// [Exchange.FlushHeader], [Exchange.WriteHeader] or [Exchange.WriteBody].
// Returns false and drops the field if the response buffer cannot fit it.
// Has no effect once the header has been written.
func (exch *Exchange) StageHeader(key, value string) (enoughMemory bool) {
if exch.headerWritten {
return false
}
off := int(exch.respHeaderOff) + int(exch.respHeaderLen)
free := len(exch.rawbuf) - off
// Field costs key+':'+value+CRLF, plus the CRLF [Exchange.FlushHeader]
// appends past the last field to close the header block.
if len(key)+len(value)+len(":\r\n")+len("\r\n") > free {
exch.respErr = lneto.ErrBufferFull // Omit writing header back to prevent incomplete response.
return false
}
n := copy(exch.rawbuf[off:], key)
if exch.normalizeKeys {
httpraw.NormalizeHeaderKey(exch.rawbuf[off : off+n])
}
exch.rawbuf[off+n] = ':'
n++
n += copy(exch.rawbuf[off+n:], value)
exch.rawbuf[off+n] = '\r'
exch.rawbuf[off+n+1] = '\n'
n += 2
exch.respHeaderLen += uint16(n)
return true
}
// StageHeaderBytes is [Exchange.StageHeader] with a byte slice value, i.e: a
// field copied out of the request. The value is not retained.
func (exch *Exchange) StageHeaderBytes(key string, value []byte) (enoughMemory bool) {
return exch.StageHeader(key, b2s(value))
}
// StageHeaderInt is [Exchange.StageHeaderIntBase] in base 10, which is the base
// every HTTP field value carrying a number uses, i.e: Content-Length.
func (exch *Exchange) StageHeaderInt(key string, value int64) (enoughMemory bool) {
return exch.StageHeaderIntBase(key, value, 10)
}
// StageHeaderIntBase is [Exchange.StageHeader] with an integer value, i.e: Content-Length.
// It formats the value directly into the response buffer without allocating.
// base must be in the range 10..36; lower bases are dropped, no HTTP header
// field value is written below base 10.
func (exch *Exchange) StageHeaderIntBase(key string, value int64, base int) (enoughMemory bool) {
if exch.headerWritten || base < 10 || base > 36 {
return false
}
off := int(exch.respHeaderOff) + int(exch.respHeaderLen)
free := len(exch.rawbuf) - off
if len(key)+internal.IntLen(value, base)+len(":\r\n")+len("\r\n") > free {
exch.respErr = lneto.ErrBufferFull // Omit writing header back to prevent incomplete response.
return false
}
n := copy(exch.rawbuf[off:], key)
if exch.normalizeKeys {
httpraw.NormalizeHeaderKey(exch.rawbuf[off : off+n])
}
exch.rawbuf[off+n] = ':'
n++
n += len(strconv.AppendInt(exch.rawbuf[off+n:off+n], value, base))
exch.rawbuf[off+n] = '\r'
exch.rawbuf[off+n+1] = '\n'
n += 2
exch.respHeaderLen += uint16(n)
return true
}
// StageStatus prepares the status line for the given code without writing
// it, i.e: "HTTP/1.1 404 Not Found". Codes with no [StatusText] get an empty
// reason phrase. Has no effect once the header has been written.
func (exch *Exchange) StageStatus(code int) {
if code >= 1000 || exch.headerWritten {
return
} else if code == 200 {
// Common case.
exch.respTopWritten = uint8(copy(exch.respTopBuf[:], "HTTP/1.1 200 OK\r\n"))
return
}
n := copy(exch.respTopBuf[:], "HTTP/1.1 ")
n += len(strconv.AppendInt(exch.respTopBuf[n:n], int64(code), 10))
text := StatusText(code)
exch.respTopBuf[n] = ' '
n++
n += copy(exch.respTopBuf[n:], text)
exch.respTopBuf[n] = '\r'
exch.respTopBuf[n+1] = '\n'
exch.respTopWritten = uint8(n + 2)
}
// WriteHeader sends the status line for code along with the staged header
// fields. Only the first call reaches the wire, as in http.ResponseWriter.
func (exch *Exchange) WriteHeader(code int) (n int, err error) {
if !exch.headerWritten {
exch.StageStatus(code)
n, err = exch.FlushHeader()
}
return n, err
}
// Respond writes a complete response in one call: Content-Type, a Content-Length
// taken from len(body), the status line and the body. An empty contentType
// stages no Content-Type field, for a code that carries no entity.
//
// It also stages "Connection: close", the router serving one exchange per
// connection, so a peer never waits on a response that is not coming.
//
// Returns [Exchange.ResponseError]: staged fields that did not fit and failed
// writes are both reported there, so a truncated response cannot pass silently.
func (exch *Exchange) Respond(code int, contentType string, body []byte) error {
exch.stageResponse(code, contentType, len(body))
exch.WriteBody(body) // Reports through respErr, checked below.
return exch.respErr
}
// RespondString is [Exchange.Respond] with a string body, saving the conversion.
func (exch *Exchange) RespondString(code int, contentType, body string) error {
exch.stageResponse(code, contentType, len(body))
exch.WriteBodyString(body) // Reports through respErr, checked below.
return exch.respErr
}
// stageResponse stages the fields and status line a complete response needs.
// Drops are recorded on respErr by the Stage* calls, so [Exchange.WriteBody]
// declines to write a partial header afterwards.
func (exch *Exchange) stageResponse(code int, contentType string, bodyLen int) {
if contentType != "" {
exch.StageHeader("Content-Type", contentType)
}
exch.StageHeaderInt("Content-Length", int64(bodyLen))
// One exchange per connection today, so the peer is told not to wait for a
// second response on it. Revisit once the router loops exchanges.
exch.StageHeader("Connection", "close")
exch.StageStatus(code)
}
// ResponseError returns any error encountered during staging of headers or during writing of response.
// Provides an ergonomic way of checking if one ran out of buffer space after staging all headers with [Exchange.StageHeader].
func (exch *Exchange) ResponseError() error {
return exch.respErr
}
// FlushHeader writes the status line and staged header fields to the connection
// and returns the bytes written, defaulting to a 200 status if none was staged.
// Does nothing if the header was already written.
func (exch *Exchange) FlushHeader() (int, error) {
if exch.respErr != nil {
return 0, exch.respErr
} else if exch.headerWritten {
return 0, nil
}
if exch.respTopWritten == 0 {
exch.StageStatus(200)
}
exch.headerWritten = true
ng, err := exch.rw.Write(exch.respTopBuf[:exch.respTopWritten])
if err != nil {
exch.respErr = err
return ng, err
}
off := int(exch.respHeaderOff)
headers := exch.rawbuf[off : off+int(exch.respHeaderLen)+2]
headers[len(headers)-1] = '\n'
headers[len(headers)-2] = '\r'
ng2, err := exch.rw.Write(headers)
exch.respErr = err
return ng + ng2, err
}
// ExchangeRW is an [io.ReadWriteCloser] view of an [Exchange] wrapping
// [Exchange.ReadBody] and [Exchange.WriteBody] methods.
//
// Exchanges are pooled and reused, so a handle records the exchange generation
// it was taken at and refuses to touch the connection once that exchange moves
// on to another request. Obtain one with [Exchange.ReadWriter].
type ExchangeRW struct {
gen uint32
exch *Exchange
}
// IsValid returns true while the handle still refers to the request it was
// taken from, i.e: false once the exchange was released.
func (rw *ExchangeRW) IsValid() bool {
return rw.gen == rw.exch.gen.Load() && rw.exch.acquired.Load()
}
func (rw *ExchangeRW) validate() error {
if !rw.IsValid() {
return net.ErrClosed
}
return nil
}
// Write writes response body bytes. See [Exchange.WriteBody].
// Fails with [net.ErrClosed] once the handle is no longer valid.
func (rw *ExchangeRW) Write(buf []byte) (int, error) {
if err := rw.validate(); err != nil {
return 0, err
}
return rw.exch.WriteBody(buf)
}
// WriteString wraps [Exchange.WriteBodyString]. Fails if handle no longer valid.
func (rw *ExchangeRW) WriteString(s string) (int, error) {
if err := rw.validate(); err != nil {
return 0, err
}
return rw.exch.WriteBodyString(s)
}
// Read reads request body bytes. See [Exchange.ReadBody].
// Fails with [net.ErrClosed] once the handle is no longer valid.
func (rw *ExchangeRW) Read(buf []byte) (int, error) {
if err := rw.validate(); err != nil {
return 0, err
}
return rw.exch.ReadBody(buf)
}
// Close invalidates this handle so later reads and writes fail. It does not
// close the connection nor end the exchange, both of which the [Router] owns.
func (rw *ExchangeRW) Close() error {
if err := rw.validate(); err != nil {
return err
}
rw.gen--
return nil
}
// ReadWriter fills dst with a stream view of the exchange, valid until the
// exchange is released. The caller owns dst, so a handler may keep one and
// refill it every request without allocating.
func (exch *Exchange) ReadWriter(dst *ExchangeRW) {
dst.gen = exch.gen.Load()
dst.exch = exch
}
// WriteBodyString implements [io.StringWriter] by unsafe conversion.
// Most underlying [io.Writer] implementations are TCP transport and not modify/own the underlying buffer.
func (exch *Exchange) WriteBodyString(buf string) (int, error) {
return exch.WriteBody(unsafe.Slice(unsafe.StringData(buf), len(buf)))
}
// WriteBody writes response body bytes, flushing the header first if the handler
// has not written it yet. Once a write to the connection fails the response is
// unrecoverable and every later write returns that same error, so a body never
// reaches the wire without its header.
func (exch *Exchange) WriteBody(buf []byte) (int, error) {
if exch.respErr != nil {
return 0, exch.respErr
} else if !exch.headerWritten {
_, err := exch.FlushHeader()
if err != nil {
return 0, err // Body must not reach the wire without its header.
}
}
if len(buf) == 0 {
return 0, nil
}
n, err := exch.rw.Write(buf)
exch.respErr = err
return n, err
}
// ReadBody reads the request body into dst, starting with the bytes that
// arrived in the same read as the header and continuing from the connection.
// The exchange does not know the body's length: use Content-Length or the
// transfer encoding to know when to stop reading.
func (exch *Exchange) ReadBody(dst []byte) (n int, _ error) {
if exch.respRemains > 0 {
toRead, err := exch.remainingSurplusBody()
if err != nil {
return 0, err
}
n = copy(dst, toRead)
exch.respRemains -= n
// hand over what already arrived since conn might have
// exhausted data and could block indefinetely.
return n, nil
}
return exch.rw.Read(dst)
}
func (exch *Exchange) remainingSurplusBody() ([]byte, error) {
_, err := exch.reqHdr.Body()
if err != nil {
return nil, err // Returns mangled buffer error if request header has been misused.
}
surplus := exch.rawbuf[exch.reqHdr.BufferParsed():exch.reqHdr.BufferReceived()]
toRead := surplus[len(surplus)-exch.respRemains:]
return toRead, nil
}
// MuxPattern returns the pattern [Mux] matched to the request.
func (exch *Exchange) MuxPattern() string {
return exch.matchedPattern
}
// RequestHeaderRaw returns the parsed request header for access beyond the
// Request* methods, such as [httpraw.Header.ForEach]. Valid until the exchange
// is released, and writing to it corrupts the response.
func (exch *Exchange) RequestHeaderRaw() *httpraw.Header {
return &exch.reqHdr
}
// RequestParseCookie parses the request's key header field into dst, i.e:
// "Cookie". The caller owns dst and its buffer, so it may be reused between
// requests.
func (exch *Exchange) RequestParseCookie(dst *httpraw.Cookie, key string) error {
value := exch.RequestHeader(key)
return dst.ParseBytes(value)
}
// RequestContentType returns the request's Content-Type field value as it
// appears on the wire, parameters included, nil if absent. Test it with
// [httpraw.MediaTypeIs] and pick parameters out with [httpraw.ContentParam].
func (exch *Exchange) RequestContentType() []byte {
// Folded: field names are case insensitive and HTTP/2 mandates lowercase, so
// a proxy translating h2 to h1 sends "content-type", RFC 9110 5.1.
return exch.RequestHeaderRaw().GetFold("Content-Type")
}
// RequestContentLength returns the body length declared by the request's
// Content-Length field. An absent field is signalled with present=false and no error.
// See [httpraw.Header.ContentLength].
func (exch *Exchange) RequestContentLength() (_ int64, present bool, _ error) {
return exch.RequestHeaderRaw().ContentLength()
}
// RequestParseForm parses "application/x-www-form-urlencoded" pairs into dst
// from the request body and, when parseURL is set, from the query string as
// well. Pairs are stored as they arrived, call [httpraw.Form.Decode] to decode
// them in place.
//
// dst owns the memory: both sources are read into its buffer and parsed together
// once. Hand it a preallocated buffer with [httpraw.Form.Reset] and turn growth
// off with [httpraw.Form.EnableBufferGrowth] to bound it, which then reports
// [httpraw.ErrBufferExhausted] instead of allocating. It grows by default.
//
// prioritizeURL reads the query ahead of the body, so a key carried by both
// resolves to the query's value: [httpraw.Form.Get] answers with the first pair
// holding a key. Both stay readable in wire order through [httpraw.Form.Pair].
// The body is consumed, so call this before [Exchange.ReadBody].
//
// A request with no Content-Length has no body, RFC 9112 6.3, and one with no
// Content-Type declares no encoding to parse, RFC 9110 8.3. Neither is an error,
// a bodiless POST being legal, and the query is still parsed when asked for. A
// Content-Type that is present and not form encoded is [errNotFormEncoded].
func (exch *Exchange) RequestParseForm(dst *httpraw.Form, parseURL, prioritizeURL bool) error {
dst.Reset(nil, 0) // Reuse whatever buffer dst holds, discarding old pairs.
if parseURL && prioritizeURL {
if err := exch.readQueryForm(dst); err != nil {
return err
}
}
if err := exch.readBodyForm(dst); err != nil {
return err
}
if parseURL && !prioritizeURL {
if err := exch.readQueryForm(dst); err != nil {
return err
}
}
return dst.Parse()
}
// formSeparator joins two sources inside one form buffer. Shared so appending it
// converts no literal per call.
var formSeparator = []byte{'&'}
// readQueryForm appends the request's query string to dst's buffer.
func (exch *Exchange) readQueryForm(dst *httpraw.Form) error {
query := exch.RequestQuery()
if len(query) == 0 {
return nil
} else if err := separateForm(dst); err != nil {
return err
}
return dst.ReadFromBytes(query)
}
// readBodyForm appends the request body to dst's buffer, reading until
// Content-Length bytes have arrived.
func (exch *Exchange) readBodyForm(dst *httpraw.Form) error {
contentType := exch.RequestContentType()
if contentType == nil {
return nil // No declared encoding is no form, RFC 9110 8.3.
} else if !httpraw.MediaTypeIs(contentType, "application/x-www-form-urlencoded") {
return errNotFormEncoded
} else if exch.RequestHeaderRaw().GetFold("Transfer-Encoding") != nil {
// Chunked bodies are framed, so reading Content-Length bytes off the
// wire would parse chunk sizes as form data. httpraw does not decode them.
return errUnsupportedTransferCoding
}
length, present, err := exch.RequestContentLength()
if err != nil {
return err
} else if !present || length == 0 {
return nil // No length is no body, RFC 9112 6.3.
}
if err = separateForm(dst); err != nil {
return err
}
// Reuse the exchange's own handle: a local would escape when boxed into the
// io.Reader [httpraw.Form.ReadLimited] takes, costing an allocation a request.
exch.ReadWriter(&exch.bodyRW)
// A single read may fall short of the limit, the body arriving a TCP segment
// at a time, so read until the declared length is in hand.
for read := 0; read < int(length); {
n, err := dst.ReadLimited(&exch.bodyRW, int(length)-read)
read += n
if n == 0 {
if err == nil {
err = io.ErrNoProgress
} else if err == io.EOF {
break // Peer sent less than it declared.
}
return err
} else if err != nil && err != io.EOF {
return err
}
}
return nil
}
// separateForm appends the '&' keeping two sources from merging into one pair,
// doing nothing while dst holds no bytes yet.
func separateForm(dst *httpraw.Form) error {
if dst.BufferUsed() == 0 {
return nil
}
return dst.ReadFromBytes(formSeparator)
}
// RequestMultipart returns a parser prepared from the boundary parameter of the
// request's Content-Type field. It reads no body: multipart parts declare no
// length, so the caller drives the loop with a buffer it owns and decides per
// part what to keep and when a part has grown too large. See
// [Exchange.ReadMultiparts] for that loop already written.
func (exch *Exchange) RequestMultipart() (mp httpraw.Multipart, err error) {
contentType := exch.RequestContentType()
if !httpraw.MediaTypeIs(contentType, "multipart/form-data") {
return mp, errNotMultipart
}
return mp, mp.SetContentType(contentType)
}
// MultipartSink is a part of a multipart body together with the writer its
// content was streamed to, as appended by [Exchange.ReadMultiparts].
type MultipartSink struct {
// Header identifies the part. Name and Filename are copies, so they
// outlive the read buffer; PartView does not, see [httpraw.MultipartHeader].
Header httpraw.MultipartHeader
// Sink received the part's content and was closed when the part ended,
// nil for a part newSink chose to discard.
Sink io.WriteCloser
}
// ReadMultiparts streams the request's "multipart/form-data" body, writing each
// part to a sink newSink returns for it and appending the pair to dst. buf is the
// only storage used and content is never held whole, so a part of any length
// streams through a buffer the caller sized. dst is appended to and returned, so
// a handler may hand back the slice of a previous request to reuse its parts.
//
// newSink is called once per part, before any of its content is read, and picks
// what to do with it from hdr.Name and hdr.Filename: return a writer to keep the
// part, or nil to discard its content and keep only the header. Each sink is
// closed as soon as its part ends, so Close reports the part arrived whole; on
// error the sink of the part being read is left open for the caller to deal with.
//
// A part header that does not fit buf is refused with [lneto.ErrShortBuffer],
// since reading more can never complete it, leaving the caller free to answer
// 413. The body is consumed, so call this before [Exchange.ReadBody].
func (exch *Exchange) ReadMultiparts(dst []MultipartSink, buf []byte, newSink func(hdr *httpraw.MultipartHeader) io.WriteCloser) (_ []MultipartSink, _ error) {
mp, err := exch.RequestMultipart()
if err != nil {
return dst, err
} else if newSink == nil || len(buf) <= len("\r\n--")+len(mp.Boundary) {
// A buffer that cannot outgrow a delimiter never makes progress.
return dst, lneto.ErrInvalidConfig
}
buflen := 0
for {
// Slot for the next part, given back when the body turns out to be
// over, so its Name and Filename buffers stay available for reuse.
part := internal.SliceReclaim(&dst)
var parsed int
for {
parsed, err = mp.NextHeader(&part.Header, buf[:buflen])
if err != nil {
dst = dst[:len(dst)-1]
if err == io.EOF {
err = nil // Closing delimiter, body done.
}
return dst, err
} else if parsed > 0 {
break // Delimiter and header block complete.
} else if buflen == len(buf) {
dst = dst[:len(dst)-1]
return dst, lneto.ErrShortBuffer // Header longer than buf.
}
// A read that both delivers and fails, as the last of the body
// followed by a hangup does, may still hold what the parser is
// waiting for: take the data and let the error surface on the
// next read.
n, readErr := exch.ReadBody(buf[buflen:])
buflen += n
if n == 0 && readErr != nil {
dst = dst[:len(dst)-1]
return dst, readErr
}
}
part.Sink = newSink(&part.Header)
buflen = copy(buf, buf[parsed:buflen])
for {
bodyLen, restOff, done := mp.NextBody(buf[:buflen])
if bodyLen > 0 && part.Sink != nil {
_, err = part.Sink.Write(buf[:bodyLen])
if err != nil {
return dst, err
}
}
buflen = copy(buf, buf[restOff:buflen])
if done {
break // Buffer now starts at the next part's delimiter.
}
n, readErr := exch.ReadBody(buf[buflen:])
buflen += n
if n == 0 && readErr != nil {
return dst, readErr // Body ended mid part.
}
}
if part.Sink != nil {
if err = part.Sink.Close(); err != nil {
return dst, err
}
}
}
}
// RequestHeader returns the value of the first request header field matching
// key, or nil if absent. Key matching is case sensitive.
func (exch *Exchange) RequestHeader(key string) []byte {
header := exch.RequestHeaderRaw()
return header.Get(key)
}
// RequestTarget returns the request-target (URI) of the request line, i.e:
// "/search?q=go". See [httpraw.Header.RequestTarget].
func (exch *Exchange) RequestTarget() []byte {
return exch.RequestHeaderRaw().RequestTarget()
}
// RequestPath returns the request-target (URI) up to the query string. This is
// what the [Mux] matches on, i.e: "/search" for a request to "/search?q=go".
func (exch *Exchange) RequestPath() []byte {
return exch.RequestHeaderRaw().RequestPath()
}
// RequestQuery returns the request's query string as it appears on the wire.
// Iterate it with [httpraw.NextQueryPair]. See [httpraw.Header.RequestQuery].
func (exch *Exchange) RequestQuery() []byte {
return exch.RequestHeaderRaw().RequestQuery()
}
// RequestQueryValue returns an undecoded view of the first query parameter
// matching key and reports whether it was present. Keys are matched decoded, so
// key "a b" finds "a%20b" and "a+b"; a parameter whose key is a malformed
// escape is skipped. A parameter with no value ("?debug") and one with an empty
// value ("?debug=") are both present with a zero length view.
//
// The view aliases the request buffer, so copy it to outlive the handler or use
// [Exchange.RequestQueryAppend] to decode it out.
func (exch *Exchange) RequestQueryValue(key string) (rawValue []byte, present bool) {
const plusAsSpace = true // Query strings are form encoded, unlike paths.
rawkey, rawval, rest := httpraw.NextQueryPair(exch.RequestQuery())
for ; rawkey != nil; rawkey, rawval, rest = httpraw.NextQueryPair(rest) {
// Compare raw first: a key needing no decoding is the common case, and
// the decoding compare walks the key an escape at a time.
if b2s(rawkey) == key || httpraw.EqualDecodedPercentURL(rawkey, key, plusAsSpace) {
return rawval, true
}
}
return nil, false
}
// RequestQueryAppend appends the value of the first query parameter matching key to
// dst and reports whether the parameter was present, matching keys as
// [Exchange.RequestQueryValue] does. A parameter with no value ("?debug") and
// one with an empty value ("?debug=") are both present with nothing appended.
//
// Values are appended raw unless decoded is set, in which case percent escapes
// and '+' are decoded. A parameter whose value fails to decode is reported
// absent, dst being left as it was rather than holding half a decode.
func (exch *Exchange) RequestQueryAppend(dst []byte, key string, decoded bool) (valueAppended []byte, present bool) {
const plusAsSpace = true // Query strings are form encoded, unlike paths.
rawval, present := exch.RequestQueryValue(key)
if !present || len(rawval) == 0 {
return dst, present
}
if !decoded {
return append(dst, rawval...), true
}
base := len(dst)
dst = slices.Grow(dst, len(rawval))
n, err := httpraw.CopyDecodedPercentURL(dst[base:base+len(rawval)], rawval, plusAsSpace)
if err != nil {
return dst[:base], false // Do not hand back half a decode.
}
return dst[:base+n], true
}
// PathValue returns the segment the request path bound to the wildcard named
// key, or nil if the matched pattern has no such wildcard. It plays the part of
// http.Request.PathValue. See [SetPathValues] for the pattern syntax and for
// which segments a wildcard binds.
//
// sm.Handle("GET /users/{id}", func(exch *httphi.Exchange) {
// id := exch.PathValue("id") // "42" on a GET /users/42.
// })
func (exch *Exchange) PathValue(key string) []byte {
for i := range exch.pathValues {
if exch.pathValues[i].Key == key {
return exch.pathValues[i].Value
} else if exch.pathValues[i].Key == "" {
break // No more keys set.
}
}
return nil
}
// PathValueAppend acceses the result of [Exchange.PathValue] and appends it to dst.
// If decoded is set to true the result will be URL-percent decoded. An error is returned if URL-percent decoding fails.
func (exch *Exchange) PathValueAppend(dst []byte, key string, decoded bool) ([]byte, error) {
const plusAsSpace = true
rawValue := exch.PathValue(key)
if !decoded || len(rawValue) == 0 {
return append(dst, rawValue...), nil
}
base := len(dst)
dst = slices.Grow(dst, len(rawValue))
n, err := httpraw.CopyDecodedPercentURL(dst[base:base+len(rawValue)], rawValue, plusAsSpace)
if err != nil {
return dst[:base], err // Do not hand back half a decode.
}
return dst[:base+n], nil
}
// RequestMethod returns the request's [Method] enum.
func (exch *Exchange) RequestMethod() Method {
return MethodFromBytes(exch.RequestMethodRaw())
}
// RequestMethod returns the request line's method as a []byte view, i.e: "GET".
func (exch *Exchange) RequestMethodRaw() []byte {
return exch.RequestHeaderRaw().Method()
}
// RequestConnectionClose returns true if the client asked for the connection to
// be closed after this exchange with a "Connection: close" header field.
func (exch *Exchange) RequestConnectionClose() bool {
return exch.RequestHeaderRaw().ConnectionClose()
}
File diff suppressed because it is too large Load Diff
-419
View File
@@ -1,419 +0,0 @@
package httphi
import (
"bytes"
"io"
"strings"
"testing"
"github.com/soypat/lneto/http/httpraw"
)
// Fuzz targets in this file are written so that a stored corpus keeps its
// meaning as the tests grow. Go's corpus files are positional and typed, so an
// input is only reproducible while the code that decodes it stays fixed. Five
// rules keep that true, and edits to this file must obey them:
//
// 1. A target's signature is frozen: func(t *testing.T, ctrl uint64, data []byte).
// Adding, removing or reordering a parameter invalidates every stored entry.
// 2. Control decisions come from ctrl only, wire bytes from data only. Never
// branch on data, never put payload in ctrl: the two axes mutate apart.
// 3. ctrl is a bit field read through absolute shifts and masks named below.
// New knobs claim unused high bits and are only ever appended, never
// renumbered, and the zero value of a knob must decode to the behaviour that
// existed before it was added, so old entries keep replaying as they did.
// 4. No PRNG, no cursor. No rand, no internal.Prand64, and no helper that
// "reads the next N bits" while advancing a position: inserting one draw
// ahead of another shifts every later decision, which is the same corpus
// invalidation a PRNG causes.
// 5. Nothing ambient: no wall clock, no goroutines, no map iteration order.
// Targets drive [Handle] on the calling goroutine; [Router] is not fuzzed
// here precisely because it serves on goroutines of its own.
const (
// Bits 0..3 index segSizes: how the request is split across reads.
ctlSegShift, ctlSegMask = 0, 0xf
// Bits 4..7 and 8..11 size the request and response halves of the buffer.
ctlReqBufShift, ctlRespBufShift, ctlBufMask = 4, 8, 0xf
// Bits 12..13 size the request header field table.
ctlKVCapShift, ctlKVCapMask = 12, 0x3
// Bit 14 normalizes outgoing header keys.
ctlNormalizeShift, ctlNormalizeMask = 14, 0x1
// Bits 15..16 make that many leading writes to the connection fail.
ctlFailWriteShift, ctlFailWriteMask = 15, 0x3
// Bits 17..20 select what the handler does, see the op constants.
ctlHandlerOpsShift, ctlHandlerOpsMask = 17, 0xf
// Bits 21..23 count the header fields the handler stages.
ctlStageCountShift, ctlStageCountMask = 21, 0x7
// Bit 24 asks AppendQuery for decoded values.
ctlDecodeShift, ctlDecodeMask = 24, 0x1
// Bits 25..28 size the scratch buffer handed to form and multipart parsing.
ctlScratchShift, ctlScratchMask = 25, 0xf
// Bit 29 makes the multipart sink discard part content.
ctlDiscardShift, ctlDiscardMask = 29, 0x1
// Next knob starts at bit 30.
)
// Handler operations, selected by the ctlHandlerOps field. Values are frozen:
// a new operation takes the next free bit within the field.
const (
opStageHeaders = 1 << iota
opReadBody
opWriteBody
opHijack
)
// segSizes are the chunk sizes a request may be delivered in, index 0 meaning
// "all at once". Splitting a request mid-CRLF or before a colon is what drives
// [httpraw.Header.TryParse]'s resumption path. Entries may be appended, never
// changed: an existing index must keep splitting exactly as it does today.
var segSizes = [16]int{0, 1, 2, 3, 5, 7, 11, 16, 23, 37, 64, 101, 173, 256, 509, 1024}
// ctlField reads a knob out of ctrl. Absolute shift, no cursor, so knobs are
// independent of one another and of the order they are read in.
func ctlField(ctrl uint64, shift, mask uint64) uint64 {
return (ctrl >> shift) & mask
}
// maxFuzzInput bounds the wire data a target accepts. The filter depends only
// on the input, so it classifies an entry the same way on every run.
const maxFuzzInput = 16 << 10
// fuzzExchange returns an exchange acquired on a connection preloaded with data,
// both sized and segmented by ctrl.
func fuzzExchange(t *testing.T, ctrl uint64, data []byte) (*Exchange, *rwconn) {
t.Helper()
reqBuf := minRequestHeaderBuffer + int(ctlField(ctrl, ctlReqBufShift, ctlBufMask))*8
respBuf := minResponseHeaderBuffer + int(ctlField(ctrl, ctlRespBufShift, ctlBufMask))*8
kvCap := 1 + int(ctlField(ctrl, ctlKVCapShift, ctlKVCapMask))*8
conn := newConn("")
seg := segSizes[ctlField(ctrl, ctlSegShift, ctlSegMask)]
if seg <= 0 || seg >= len(data) {
conn.AddReadable(data)
} else {
for off := 0; off < len(data); off += seg {
conn.AddSegment(string(data[off:min(off+seg, len(data))]))
}
}
// Always hang up: a drained connection that never reports EOF reads (0,nil)
// forever and [Handle] would back off in an unbounded loop.
conn.Hangup()
if n := ctlField(ctrl, ctlFailWriteShift, ctlFailWriteMask); n > 0 {
conn.FailWrites(int(n))
}
exch := newExchange(t, conn, ExchangeConfig{
RawBuf: make([]byte, reqBuf+respBuf),
RequestBufferLim: reqBuf,
NumHeaderKVCap: kvCap,
NormalizeOutgoingKeys: ctlField(ctrl, ctlNormalizeShift, ctlNormalizeMask) != 0,
NoRequestBufferGrowth: true,
})
return exch, conn
}
// checkRequestView asserts the request-target views agree with one another.
func checkRequestView(t *testing.T, exch *Exchange) {
t.Helper()
target, path, query := exch.RequestTarget(), exch.RequestPath(), exch.RequestQuery()
if !bytes.HasPrefix(target, path) {
t.Fatalf("path %q is not a prefix of target %q", path, target)
}
if len(query) > 0 && !bytes.HasSuffix(target, query) {
t.Fatalf("query %q is not a suffix of target %q", query, target)
}
if len(path)+len(query) > len(target) {
t.Fatalf("path %q and query %q exceed target %q", path, query, target)
}
}
// checkResponse asserts that whatever reached the wire is a response a peer
// could parse: a status line this package produced, followed by a header block
// that terminates, and that [httpraw] reads back what it wrote.
func checkResponse(t *testing.T, written string) {
t.Helper()
if written == "" {
return // Hijacked, or a request refused before anything was staged.
}
const proto = "HTTP/1.1 "
if !strings.HasPrefix(written, proto) {
t.Fatalf("response does not open with a status line: %q", written)
}
if len(written) < len(proto)+4 {
t.Fatalf("status line truncated: %q", written)
}
for _, c := range []byte(written[len(proto) : len(proto)+3]) {
if c < '0' || c > '9' {
t.Fatalf("status code is not three digits: %q", written)
}
}
if written[len(proto)+3] != ' ' {
t.Fatalf("status code not followed by a space: %q", written)
}
if !strings.Contains(written, "\r\n\r\n") {
t.Fatalf("header block never terminated: %q", written)
}
var resp httpraw.Header
const asResponse = true
if err := resp.ParseBytes(asResponse, []byte(written)); err != nil {
t.Fatalf("response does not parse back: %s in %q", err, written)
}
}
// handler2Path is a second registration so lookup does not always match on the
// first entry of the mux.
const handler2Path = "/fuzz"
// fuzzMux returns a mux serving handler on the paths the seed corpus requests.
func fuzzMux(handler HandlerFunc) *MuxSlice {
var mux MuxSlice
mux.Handle("/", handler)
mux.Handle(handler2Path, handler)
return &mux
}
// FuzzHandleRequest drives a whole exchange: a request off the wire through
// [Handle], a handler staging and writing a response, and back out to the peer.
func FuzzHandleRequest(f *testing.F) {
addSeeds(f)
f.Fuzz(func(t *testing.T, ctrl uint64, data []byte) {
if len(data) > maxFuzzInput {
return
}
exch, conn := fuzzExchange(t, ctrl, data)
ops := ctlField(ctrl, ctlHandlerOpsShift, ctlHandlerOpsMask)
stage := ctlField(ctrl, ctlStageCountShift, ctlStageCountMask)
Handle(exch, fuzzMux(func(exch *Exchange) {
checkRequestView(t, exch)
if ops&opStageHeaders != 0 {
// Literal fields: staging request bytes would test the caller's
// escaping, not this package's framing.
for i := range stage {
exch.StageHeader("X-Fuzz", "value")
exch.StageHeaderIntBase("X-Fuzz-Int", int64(i), 10)
}
}
if ops&opReadBody != 0 {
var body [64]byte
for range 64 {
n, err := exch.ReadBody(body[:])
if n == 0 && err != nil {
break
}
}
}
if ops&opWriteBody != 0 {
exch.WriteBody([]byte("fuzz body"))
}
if ops&opHijack != 0 {
exch.HijackRaw(nil)
}
}), nopBackoff)
if ctlField(ctrl, ctlFailWriteShift, ctlFailWriteMask) == 0 {
// A refused write leaves a partial response on purpose, so the
// wire is only well formed when every write got through.
checkResponse(t, conn.ViewWritten())
}
})
}
// FuzzQueryAndForm drives the request-target query and the form-encoded body,
// both of which decode percent escapes in place over caller memory.
func FuzzQueryAndForm(f *testing.F) {
addSeeds(f)
f.Fuzz(func(t *testing.T, ctrl uint64, data []byte) {
if len(data) > maxFuzzInput {
return
}
exch, _ := fuzzExchange(t, ctrl, data)
decoded := ctlField(ctrl, ctlDecodeShift, ctlDecodeMask) != 0
scratchLen := 8 + int(ctlField(ctrl, ctlScratchShift, ctlScratchMask))*16
Handle(exch, fuzzMux(func(exch *Exchange) {
// Every pair the iterator yields must be reachable by name, or the
// two views of the query string disagree.
const maxPairs = 64
key, _, rest := httpraw.NextQueryPair(exch.RequestQuery())
for pairs := 0; key != nil && pairs < maxPairs; pairs++ {
dec := make([]byte, len(key))
n, err := httpraw.CopyDecodedPercentURL(dec, key, true)
if err == nil {
if n > len(key) {
t.Fatalf("decoding key %q grew it to %d bytes", key, n)
}
if _, present := exch.RequestQueryAppend(nil, string(dec[:n]), decoded); !present {
t.Fatalf("query pair %q absent from AppendQuery", key)
}
}
key, _, rest = httpraw.NextQueryPair(rest)
}
var form httpraw.Form
buf := make([]byte, scratchLen)
if err := exch.RequestParseForm(&form, false, false); err != nil {
return
}
total := 0
lens := make([]int, form.Len())
for i := range form.Len() {
k, v := form.Pair(i)
lens[i] = len(k) + len(v)
total += lens[i]
}
if total > len(buf) {
t.Fatalf("form pairs span %d bytes of a %d byte buffer", total, len(buf))
}
if err := form.Decode(); err != nil {
return
}
// Decoding replaces escapes in place, so no pair may grow.
for i := range form.Len() {
k, v := form.Pair(i)
if len(k)+len(v) > lens[i] {
t.Fatalf("pair %d grew from %d to %d bytes on decode", i, lens[i], len(k)+len(v))
}
}
}), nopBackoff)
})
}
// countSink counts what a multipart part streamed into it and whether the part
// was closed off.
type countSink struct {
written int
closed bool
}
func (c *countSink) Write(b []byte) (int, error) {
c.written += len(b)
return len(b), nil
}
func (c *countSink) Close() error {
c.closed = true
return nil
}
// FuzzMultipart drives [Exchange.ReadMultiparts], which streams a body of
// unknown length through a buffer the caller sized.
func FuzzMultipart(f *testing.F) {
addSeeds(f)
f.Fuzz(func(t *testing.T, ctrl uint64, data []byte) {
if len(data) > maxFuzzInput {
return
}
exch, _ := fuzzExchange(t, ctrl, data)
discard := ctlField(ctrl, ctlDiscardShift, ctlDiscardMask) != 0
bufLen := 16 + int(ctlField(ctrl, ctlScratchShift, ctlScratchMask))*16
Handle(exch, fuzzMux(func(exch *Exchange) {
var sinks []*countSink
_, err := exch.ReadMultiparts(nil, make([]byte, bufLen), func(hdr *httpraw.MultipartHeader) io.WriteCloser {
if discard {
return nil
}
sink := new(countSink)
sinks = append(sinks, sink)
return sink
})
if err != nil {
return // Sinks are left for the caller to deal with on error.
}
total := 0
for _, sink := range sinks {
if !sink.closed {
t.Fatal("ReadMultiparts returned with a part left open")
}
total += sink.written
}
if total > len(data) {
t.Fatalf("parts streamed %d bytes out of a %d byte request", total, len(data))
}
}), nopBackoff)
})
}
// addSeeds adds the shared seed corpus. Every seed pins ctrl to zero so its
// meaning never moves: knobs added later decode their zero value to the
// behaviour the seed was recorded under.
func addSeeds(f *testing.F) {
f.Helper()
const (
formType = "Content-Type: application/x-www-form-urlencoded\r\n"
mpType = "Content-Type: multipart/form-data; boundary=b0undary\r\n"
)
seeds := []string{
// Well formed traffic, so the fuzzer has somewhere to mutate from.
"GET / HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /fuzz?q=go&n=1 HTTP/1.1\r\nHost: h\r\n\r\n",
"POST /fuzz HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 11\r\n\r\na=1&b=2&c=3",
"POST /fuzz HTTP/1.1\r\nHost: h\r\n" + mpType + "Content-Length: 76\r\n\r\n--b0undary\r\nContent-Disposition: form-data; name=\"f\"\r\n\r\nbody\r\n--b0undary--\r\n",
// Framing the RFC leaves room to disagree over, which is where request
// smuggling lives: two lengths, a length plus a coding, and a coding
// this package does not decode.
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 3\r\nContent-Length: 4\r\n\r\nabcd",
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 3\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n0\r\n\r\n",
"POST / HTTP/1.1\r\nHost: h\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nbody\r\n0\r\n\r\n",
// Field names are case insensitive, RFC 9110 5.1, so a lookup that
// misses one of these reads the request differently than the peer wrote it.
"POST / HTTP/1.1\r\nHost: h\r\ncontent-length: 4\r\n\r\nbody",
"POST / HTTP/1.1\r\nHost: h\r\nCONTENT-LENGTH: 4\r\n\r\nbody",
"POST /fuzz HTTP/1.1\r\nHost: h\r\ncontent-type: application/x-www-form-urlencoded\r\ncontent-length: 3\r\n\r\na=1",
// Content-Length values that are not a bare digit string, RFC 9112 6.2.
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: +5\r\n\r\nbody!",
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: -1\r\n\r\nbody",
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 1 2\r\n\r\nbody",
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length:\r\n\r\nbody",
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 9223372036854775808\r\n\r\nbody",
// Line ending and field syntax edges.
"GET / HTTP/1.1\nHost: h\n\n",
"GET / HTTP/1.1\r\nHost: h\rX: y\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\n Continued: fold\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\nX: va\x00lue\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\nNoColon\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\n: novalue\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\nX:\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\n\r\n\r\n",
// Request lines this package tolerates or must refuse.
"GET http://h/abs HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /\r\n\r\n", // HTTP/0.9 simple request, no version.
" GET / HTTP/1.1\r\nHost: h\r\n\r\n",
"GET / HTTP/1.1\r\nHost: h\r\n\r\n",
"/ HTTP/1.1\r\nHost: h\r\n\r\n",
"GET / HTTP/9.9\r\nHost: h\r\n\r\n",
// Percent escapes, including the truncated and the malformed.
"GET /a%20b?a%20b=c%20d HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /?q=%2 HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /?q=%zz HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /%00?a=%00 HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /?a=b=c&&=v&debug HTTP/1.1\r\nHost: h\r\n\r\n",
"GET /?a%3db=1&a+b=2 HTTP/1.1\r\nHost: h\r\n\r\n",
// Multipart bodies that end early or never open a part.
"POST /fuzz HTTP/1.1\r\nHost: h\r\n" + mpType + "Content-Length: 12\r\n\r\n--b0undary\r\n",
"POST /fuzz HTTP/1.1\r\nHost: h\r\n" + mpType + "Content-Length: 14\r\n\r\n--b0undary--\r\n",
"POST /fuzz HTTP/1.1\r\nHost: h\r\nContent-Type: multipart/form-data\r\nContent-Length: 4\r\n\r\nbody",
// Sizes that crowd the buffers: a long target, many fields, and a body
// arriving in the same read as the header it follows.
"GET /" + strings.Repeat("a", 512) + " HTTP/1.1\r\nHost: h\r\n\r\n",
"GET / HTTP/1.1\r\n" + strings.Repeat("X: y\r\n", 200) + "\r\n",
"GET / HTTP/1.1\r\n" + strings.Repeat("k", 512) + ": v\r\n\r\n",
"POST / HTTP/1.1\r\nHost: h\r\nContent-Length: 4\r\n\r\nbodyTRAILING",
// Nothing, and nothing that resembles a request at all.
"",
"\r\n\r\n",
"\x00\x00\x00\x00",
}
for _, seed := range seeds {
f.Add(uint64(0), []byte(seed))
}
}
-415
View File
@@ -1,415 +0,0 @@
package httphi
import (
"bytes"
"io"
"strings"
"unsafe"
"github.com/soypat/lneto"
"github.com/soypat/lneto/http/httpraw"
"github.com/soypat/lneto/internal"
)
// Handle is a extremely low-level HTTP handling method used internally in [Router].
// Requires exchange to be acquired and configured. Will panic if any argument is nil.
// Handle does not close the connection on any outcome: the caller owns it.
// backoff can be set for dealing with non-blocking connections. If backoff set to nil
// then a zero-length-read will result in Handle returning [io.ErrNoProgress].
func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
if !exch.acquired.Load() {
return lneto.ErrBadState
}
reqhdr := &exch.reqHdr
reqhdr.Reset(nil, 0) // Assume exchange has been configured and reuse memory.
var consecutiveBackoffs uint
for {
n, err := reqhdr.ReadFromLimited(exch.rw, reqhdr.BufferFree())
if err != nil {
exch.readErr = err
exch.handleError(err)
return err
} else if n == 0 {
if backoff == nil {
return io.ErrNoProgress
}
backoff.Do(consecutiveBackoffs)
consecutiveBackoffs++
continue
}
consecutiveBackoffs = 0
const asRequest = false
needMore, err := reqhdr.TryParse(asRequest)
if needMore {
continue // Request header split across reads, accumulate the rest.
} else if err != nil {
exch.handleError(err)
return err
}
break // Done!
}
// Setup Exchange fields necessary for correct functioning.
parsed := reqhdr.BufferParsed()
exch.respRemains = reqhdr.BufferReceived() - parsed
exch.respHeaderOff = uint16(parsed)
exch.respHeaderLen = 0
proto := b2s(reqhdr.Protocol())
if len(proto) == 0 {
// HTTP/0.9 not tolerated RFC 9112 3.
exch.WriteHeader(int(StatusBadRequest))
return errNoRequestProto
} else if proto != "HTTP/1.1" && proto != "HTTP/1.0" {
// RFC 9112 2.6.
exch.WriteHeader(int(StatusHTTPVersionNotSupported))
return errBadRequestProto
}
// Mux on the request path: the query string is the handler's business.
path := reqhdr.RequestPath()
meth := reqhdr.Method()
clear(exch.pathValues)
matchedPattern, handler := mux.LookupHandler(MethodFromBytes(meth), path, exch.pathValues)
if handler != nil {
exch.matchedPattern = matchedPattern
handler(exch)
if !exch.hijacked {
exch.FlushHeader()
}
} else {
exch.WriteHeader(404)
}
return nil
}
func (exch *Exchange) handleError(err error) {
if err == httpraw.ErrHeaderTooMany || err == httpraw.ErrBufferExhausted || exch.reqHdr.BufferFree() == 0 {
// The peer is owed an answer: no larger buffer is coming, so
// say so instead of dropping the connection, RFC 6585 5.
exch.StageHeader("Content-Length", "0")
exch.WriteHeader(int(StatusRequestHeaderFieldsTooLarge))
}
}
// HandlerFunc serves a single request, playing the part of http.Handler.
// The exchange is only valid for the duration of the call: it is released to
// the router's pool on return, so a handler must not retain it nor any slice it
// handed out.
type HandlerFunc func(ex *Exchange)
// Mux resolves a request to the handler that serves it. [Handle] calls
// LookupHandler with the request-target's path, not the whole target, and
// replies 404 when it returns nil.
type Mux interface {
// LookupHandler matches the requestPath and method to a handler and returns it and the
// pattern it matched. dstPathVals are set to non-zero values by Mux and can later be accessed by [Exchange.PathValue]
// requestPath is a buffer owned by the [Exchange] usually and should not be held after LookupHandler returns.
LookupHandler(get Method, requestPath []byte, dstPathVals []PathValue) (matchedPattern string, handler HandlerFunc)
// MaxPathValues specifies the required size of dstPathVals in a call to [Mux.LookupHandler].
// MaxPathValues should return -1 if no paths have been configured to catch situation
// where the Mux has been passed to a [Router.Configuration] before registering paths.
MaxPathValues() int
}
// PathValue used to implement [Mux] interface. Stores http.Request.PathValue-like values.
type PathValue struct {
Key string // owned by mux.
Value []byte // points to raw exchange buffer.
}
// pathSeparator is shared so [SetPathValues] never converts a literal per call.
var pathSeparator = []byte{'/'}
// SetPathValues matches requestPath against pattern and binds its wildcards
// into dstPathVals, read back with [Exchange.PathValue]. Wildcards are whole
// segments as per http.ServeMux: "{name}" takes one non-empty segment,
// "{name...}" the rest including slashes, "{$}" only the path's end, and a
// trailing slash is an anonymous "{...}". i.e: "/b/{bucket}/o/{obj...}".
//
// Unlike ServeMux, segments are compared and bound raw, so "/users/{id}" binds
// "x%2Fy" and not "x/y". Which paths match is unaffected. Bound values alias
// requestPath rather than copy it.
//
// Values are bound while walking, before the match is known, so on failure
// SetPathValues clears what it bound. A [Mux] may then try patterns in turn
// without a matching one inheriting values from one that failed.
func SetPathValues(dstPathVals []PathValue, pattern string, requestPath []byte) (matched, pathValSliceTooShort bool) {
n, matched, pathValSliceTooShort := setPathValues(dstPathVals, pattern, requestPath)
if !matched {
clear(dstPathVals[:n])
}
return matched, pathValSliceTooShort
}
// setPathValues is [SetPathValues] reporting how many values it bound, so its
// caller can discard them when the pattern turns out not to match.
func setPathValues(dstPathVals []PathValue, pattern string, requestPath []byte) (n int, matched, pathValSliceTooShort bool) {
if len(pattern) == 0 || pattern[0] != '/' || len(requestPath) == 0 || requestPath[0] != '/' {
return n, false, false
}
pattern, requestPath = pattern[1:], requestPath[1:]
for {
if len(pattern) == 0 {
// Nothing left after a slash: an anonymous "..." taking the rest,
// which is why "/files/" matches "/files/a/b" and "/" matches all.
return n, true, false
}
patSeg, patRest, patMore := strings.Cut(pattern, "/")
reqSeg, reqRest, reqMore := bytes.Cut(requestPath, pathSeparator)
name, isMulti, isWildcard := pathWildcard(patSeg)
switch {
case isWildcard && name == "$":
// Matches the end of the path and nothing else, so it must be the
// last segment of the pattern and leave no path behind.
return n, !patMore && len(requestPath) == 0, false
case isWildcard && isMulti:
// Takes the remainder including slashes, possibly empty.
if name != "" {
if n >= len(dstPathVals) {
return n, false, true
}
dstPathVals[n] = PathValue{Key: name, Value: requestPath}
n++
}
return n, true, false
case isWildcard:
if len(reqSeg) == 0 {
return n, false, false // One segment means a non-empty one.
}
if n >= len(dstPathVals) {
return n, false, true
}
dstPathVals[n] = PathValue{Key: name, Value: reqSeg}
n++
default:
if b2s(reqSeg) != patSeg {
return n, false, false
}
}
if patMore != reqMore {
// One side has a further segment and the other does not, so
// "/health" misses "/health/" and "/files/" misses "/files".
return n, false, false
} else if !patMore {
return n, true, false // Both spent on the same segment.
}
pattern, requestPath = patRest, reqRest
}
}
// pathWildcard picks apart a "{name}" or "{name...}" pattern segment. It
// reports ok false for a literal segment, so "/b_{bucket}" is literal text and
// not a wildcard, matching ServeMux's rule that wildcards be whole segments.
func pathWildcard(segment string) (name string, isMulti, ok bool) {
if len(segment) < 2 || segment[0] != '{' || segment[len(segment)-1] != '}' {
return "", false, false
}
name = segment[1 : len(segment)-1]
if rest, found := strings.CutSuffix(name, "..."); found {
return rest, true, true
}
return name, false, true
}
// MuxSlice is a [Mux] implementation backed by a slice of registered endpoints, matched by
// exact path. Lookup is linear in the number of registrations.
type MuxSlice struct {
// TODO: binary search worth it?
_handlers []struct {
method Method
path string
handler HandlerFunc
pathVals int
}
}
// Reset discards all registered handlers, reusing the backing array and growing
// it to fit capacity registrations.
func (sm *MuxSlice) Reset(capacity int) {
internal.SliceReuse(&sm._handlers, capacity)
}
// LookupHandler returns the handler registered for request path, or nil if none matches.
// The first registration matching both method and uri wins.
//
// Every method this package does not name is [MethUnknown], so a request with an
// extension method matches a bare-path registration and any registration naming
// an extension method, whichever it names. Tell PROPFIND from MKCOL inside the
// handler with [Exchange.RequestMethodRaw].
func (sm *MuxSlice) LookupHandler(method Method, path []byte, dstPathVals []PathValue) (matched string, _ HandlerFunc) {
for _, endpoint := range sm._handlers {
if endpoint.method != MethUndefined && endpoint.method != method {
continue
}
// Method matches. A pattern ending in '/' is a wildcard despite binding no
// values: the trailing slash is an anonymous "{...}", so it must go
// through the matcher and not a literal compare, see [SetPathValues].
if isWildcardPattern(endpoint.path) {
if ok, _ := SetPathValues(dstPathVals, endpoint.path, path); ok {
return endpoint.path, endpoint.handler
}
} else if b2s(path) == endpoint.path {
return endpoint.path, endpoint.handler
}
}
return "", nil
}
// MaxPathValues returns the maximum number of path values any endpoint could have.
func (sm *MuxSlice) MaxPathValues() (maxPathValues int) {
if len(sm._handlers) == 0 {
return -1 // Signal no handlers registered.
}
for _, endpoint := range sm._handlers {
maxPathValues = max(maxPathValues, endpoint.pathVals)
}
return maxPathValues
}
// Handle registers handler for reg, either a bare path matching any method or a
// method and path separated by a space, i.e: "/health" or "GET /health".
//
// Handle panics on a registration that could never serve a request: a method
// token carrying lowercase (methods are case sensitive and uppercase, RFC 9110
// 9.1, so "Get" matches no GET request), a path not rooted at '/', or an exact
// duplicate of an earlier registration, which the first one always shadows.
// Registration is program startup, so a fault belongs there and not in a
// permanent silent 404.
func (sm *MuxSlice) Handle(optMethodAndPath string, handler HandlerFunc) {
method := MethUndefined
methodOrURL, url, methodFound := strings.Cut(optMethodAndPath, " ")
if methodFound {
if hasLowerASCII(methodOrURL) {
panic("httphi: method must be uppercase in registration " + optMethodAndPath)
}
method = MethodFrom(methodOrURL)
} else {
url = methodOrURL
}
if len(url) == 0 || url[0] != '/' {
panic("httphi: path must begin with '/' in registration " + optMethodAndPath)
}
for _, endpoint := range sm._handlers {
if endpoint.method == method && endpoint.path == url {
if method == MethUnknown {
// Two extension methods are both MethUnknown, so the second is
// unreachable. Register one and branch in the handler, see
// [MuxSlice.LookupHandler].
panic("httphi: extension method already registered on path in " + optMethodAndPath)
}
panic("httphi: duplicate registration " + optMethodAndPath)
}
}
v := internal.SliceReclaim(&sm._handlers)
v.pathVals = countPathValues(url)
v.method = method
v.path = url
v.handler = handler
}
// countPathValues is how many values pattern can bind, which is what sizes the
// slice [SetPathValues] writes into. Only a named wildcard segment binds: "{$}"
// marks the path's end, an anonymous "{...}" has no name to bind under, and a
// brace inside a literal segment is not a wildcard at all.
func countPathValues(pattern string) (n int) {
if len(pattern) == 0 || pattern[0] != '/' {
return 0
}
pattern = pattern[1:]
for len(pattern) > 0 {
segment, rest, more := strings.Cut(pattern, "/")
if name, _, ok := pathWildcard(segment); ok && name != "" && name != "$" {
n++
}
if !more {
break
}
pattern = rest
}
return n
}
// isWildcardPattern reports whether pattern must go through [SetPathValues]
// rather than a literal comparison. Distinct from the value count: "{$}" and a
// trailing slash match by walking segments while binding nothing.
func isWildcardPattern(pattern string) bool {
return strings.IndexByte(pattern, '{') >= 0 || strings.HasSuffix(pattern, "/")
}
// hasLowerASCII reports whether s carries an ASCII lowercase letter, which a
// method token registered by mistake ("Get") does and a legal extension method
// ("PROPFIND") does not.
func hasLowerASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 'a' && s[i] <= 'z' {
return true
}
}
return false
}
// Method is a HTTP request method, parsed by [MethodFrom].
type Method uint8
const (
MethUndefined Method = iota // undefined
MethGet // GET
// lol.
MethHead // HEAD
MethPost // POST
MethPut // PUT
// RFC 5789
MethPatch // PATCH
MethDelete // DELETE
MethConnect // CONNECT
MethOptions // OPTIONS
MethTrace // TRACE
MethUnknown // unknown
)
// MethodFrom returns the [Method] matching meth, [MethUndefined] if meth is
// empty and [MethUnknown] if it names a method this package does not know.
// Comparison is case sensitive: methods are uppercase, RFC 9110 9.1.
func MethodFrom(meth string) (res Method) {
if len(meth) == 0 {
return MethUndefined
}
switch meth {
case "GET":
res = MethGet
case "HEAD":
res = MethHead
case "POST":
res = MethPost
case "PUT":
res = MethPut
case "PATCH":
res = MethPatch
case "DELETE":
res = MethDelete
case "CONNECT":
res = MethConnect
case "OPTIONS":
res = MethOptions
case "TRACE":
res = MethTrace
default:
res = MethUnknown
}
return res
}
// MethodFromBytes is a [MethodFrom] wrapper with bytes argument instead of string.
func MethodFromBytes(meth []byte) (res Method) {
if len(meth) == 0 {
return MethUndefined
}
return MethodFrom(b2s(meth))
}
// b2s converts byte slice to a string without memory allocation.
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
func b2s(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
-436
View File
@@ -1,436 +0,0 @@
package httphi
import (
"strings"
"testing"
)
// SetPathValues must agree with net/http.ServeMux on which patterns match which
// paths and what each wildcard binds to. Every case below was taken from a run
// against a real ServeMux, so this table is an oracle, not a guess.
//
// The one documented deviation is percent-decoding: ServeMux unescapes segments
// before matching and binding, this does not. See TestSetPathValuesEscaping.
func TestSetPathValues(t *testing.T) {
for _, test := range []struct {
pattern string
path string
match bool
want string // "name=value" pairs joined by "|", in bind order.
}{
// Single segment wildcard binds exactly one non-empty segment.
{pattern: "/users/{id}", path: "/users/42", match: true, want: "id=42"},
{pattern: "/users/{id}", path: "/users/42/x", match: false},
{pattern: "/users/{id}", path: "/users/", match: false},
{pattern: "/users/{id}", path: "/users", match: false},
{pattern: "/users/{id}/edit", path: "/users/42/edit", match: true, want: "id=42"},
{pattern: "/{a}/{b}", path: "/x/y", match: true, want: "a=x|b=y"},
// "..." swallows the remainder, slashes included, and may bind empty.
{pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o/a/b/c", match: true, want: "bucket=bk|obj=a/b/c"},
{pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o/", match: true, want: "bucket=bk|obj="},
{pattern: "/b/{bucket}/o/{obj...}", path: "/b/bk/o", match: false},
{pattern: "/files/{p...}", path: "/files/", match: true, want: "p="},
{pattern: "/files/{p...}", path: "/files", match: false},
// {$} matches only the end of the path.
{pattern: "/{$}", path: "/", match: true},
{pattern: "/{$}", path: "/x", match: false},
{pattern: "/a/{$}", path: "/a/", match: true},
{pattern: "/a/{$}", path: "/a", match: false},
{pattern: "/a/{$}", path: "/a/b", match: false},
// A trailing slash is an anonymous "..." wildcard, binding nothing.
{pattern: "/files/", path: "/files/a/b", match: true},
{pattern: "/files/", path: "/files/", match: true},
{pattern: "/files/", path: "/files", match: false},
{pattern: "/", path: "/anything/at/all", match: true},
// Literal patterns match exactly, trailing slash included.
{pattern: "/health", path: "/health", match: true},
{pattern: "/health", path: "/health/", match: false},
// An empty segment never satisfies a single wildcard.
{pattern: "/a/{x}/b", path: "/a//b", match: false},
} {
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
vals := make([]PathValue, 8)
match, tooShort := SetPathValues(vals, test.pattern, []byte(test.path))
if tooShort {
t.Fatal("8 slots must be enough for these patterns")
}
if match != test.match {
t.Fatalf("want match=%v, got %v", test.match, match)
}
if !match {
return
}
if got := renderPathValues(vals); got != test.want {
t.Errorf("want %q, got %q", test.want, got)
}
})
}
}
// Bound values must alias the request path buffer rather than copy it: the
// exchange owns that memory and a copy would allocate per request.
func TestSetPathValuesAliasesRequestBuffer(t *testing.T) {
path := []byte("/users/42/edit")
vals := make([]PathValue, 4)
match, _ := SetPathValues(vals, "/users/{id}/edit", path)
if !match {
t.Fatal("want match")
}
if string(vals[0].Value) != "42" {
t.Fatalf("want id=42, got %q", vals[0].Value)
}
// Mutating the request buffer must show through the bound value.
path[7] = '9'
if string(vals[0].Value) != "92" {
t.Errorf("value must alias the request buffer, got %q", vals[0].Value)
}
}
// A destination too small to hold every wildcard must say so rather than bind a
// partial set or write out of range.
func TestSetPathValuesSliceTooShort(t *testing.T) {
match, tooShort := SetPathValues(make([]PathValue, 1), "/{a}/{b}", []byte("/x/y"))
if !tooShort {
t.Error("want pathValSliceTooShort for 2 wildcards in 1 slot")
}
if match {
t.Error("want match=false when values could not be bound")
}
// A pattern that binds nothing needs no slots at all.
match, tooShort = SetPathValues(nil, "/health", []byte("/health"))
if !match || tooShort {
t.Errorf("want match with no slots needed, got match=%v tooShort=%v", match, tooShort)
}
}
// Percent escapes are compared and bound raw. ServeMux unescapes segment by
// segment, so "/users/x%2Fy" binds id="x/y" there and id="x%2Fy" here. Matching
// agrees either way; only the bound bytes differ.
func TestSetPathValuesEscaping(t *testing.T) {
vals := make([]PathValue, 4)
if match, _ := SetPathValues(vals, "/a%2Fb/{x}", []byte("/a%2Fb/v")); !match {
t.Error("want literal escape in pattern to match the same bytes in path")
}
vals = make([]PathValue, 4)
match, _ := SetPathValues(vals, "/users/{id}", []byte("/users/x%2Fy"))
if !match {
t.Fatal("want match")
}
if got := string(vals[0].Value); got != "x%2Fy" {
t.Errorf("want raw %q, got %q", "x%2Fy", got)
}
}
// renderPathValues joins the bound pairs for comparison, stopping at the first
// unused slot.
func renderPathValues(vals []PathValue) string {
var sb strings.Builder
for _, v := range vals {
if v.Key == "" {
break
}
if sb.Len() > 0 {
sb.WriteByte('|')
}
sb.WriteString(v.Key)
sb.WriteByte('=')
sb.Write(v.Value)
}
return sb.String()
}
// Matching a request must not allocate: keys alias the mux's pattern and values
// alias the request buffer, so nothing is copied per request.
func TestSetPathValuesNoAlloc(t *testing.T) {
vals := make([]PathValue, 8)
path := []byte("/b/bk/o/a/b/c")
allocs := testing.AllocsPerRun(100, func() {
SetPathValues(vals, "/b/{bucket}/o/{obj...}", path)
})
if allocs != 0 {
t.Fatalf("SetPathValues allocated %v times, want 0", allocs)
}
}
// pathValueMux binds one wildcard pattern, standing in for a [Mux] that
// supports them until MuxSlice sets setPathVal.
type pathValueMux struct {
pattern string
handler HandlerFunc
}
func (m *pathValueMux) MaxPathValues() int {
return -1
}
func (m *pathValueMux) LookupHandler(method Method, path []byte, dst []PathValue) (string, HandlerFunc) {
if ok, _ := SetPathValues(dst, m.pattern, path); ok {
return m.pattern, m.handler
}
return "", nil
}
// A wildcard bound by one request must not be readable by the next request the
// same pooled exchange serves. A literal pattern binds nothing, so it never
// overwrites the previous request's slots, and the values it would leak alias a
// buffer the new request has already overwritten.
func TestExchangePathValueClearedBetweenRequests(t *testing.T) {
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: 4,
})
// First request binds id=42 off a wildcard pattern.
var gotFirst string
wildcard := &pathValueMux{pattern: "/users/{id}", handler: func(e *Exchange) {
gotFirst = string(e.PathValue("id"))
e.WriteHeader(200)
}}
conn := newConn("GET /users/42 HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
if !exch.Acquire(conn) {
t.Fatal("fresh exchange failed to acquire")
}
if err := Handle(exch, wildcard, nopBackoff); err != nil {
t.Fatalf("first request: %s", err)
}
exch.Release()
if gotFirst != "42" {
t.Fatalf("want id=42 bound on the first request, got %q", gotFirst)
}
// Second request matches a literal pattern, which binds nothing at all.
var leaked []byte
var sm MuxSlice
sm.Handle("/health", func(e *Exchange) {
leaked = e.PathValue("id")
e.WriteHeader(200)
})
conn2 := newConn("GET /health HTTP/1.1\r\nHost: h\r\n\r\n")
conn2.Hangup()
if !exch.Acquire(conn2) {
t.Fatal("released exchange failed to re-acquire")
}
if err := Handle(exch, &sm, nopBackoff); err != nil {
t.Fatalf("second request: %s", err)
}
if leaked != nil {
t.Errorf("want no path value on a literal route, got id=%q from the previous request", leaked)
}
}
// A lookup tries each endpoint in turn and [SetPathValues] binds as it walks, so
// a pattern that binds values and then fails must not leave them behind for the
// pattern that does match: a handler would read a wildcard no matched pattern has.
func TestMuxSliceNoStaleBindingsAcrossCandidates(t *testing.T) {
var sm MuxSlice
sm.Reset(2)
sm.Handle("/a/{x}/{y}/z", func(ex *Exchange) { t.Error("non-matching handler ran") })
var gotP, gotX, gotY string
sm.Handle("/a/{p}/b", func(ex *Exchange) {
gotP = string(ex.PathValue("p"))
gotX = string(ex.PathValue("x"))
gotY = string(ex.PathValue("y"))
ex.WriteHeader(200)
})
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: sm.MaxPathValues(),
})
conn := newConn("GET /a/1/b HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
if !exch.Acquire(conn) {
t.Fatal("fresh exchange failed to acquire")
}
if err := Handle(exch, &sm, nopBackoff); err != nil {
t.Fatal(err)
}
if gotP != "1" {
t.Errorf("want p=1 from the matched pattern, got %q", gotP)
}
if gotX != "" || gotY != "" {
t.Errorf("want no x/y from the failed candidate, got x=%q y=%q", gotX, gotY)
}
}
// A pattern ending in '/' carries no brace but is still a wildcard: the trailing
// slash is an anonymous "{...}", see [SetPathValues]. MuxSlice must route it
// through the same matcher rather than comparing the path literally.
func TestMuxSliceTrailingSlashPattern(t *testing.T) {
for _, test := range []struct {
pattern string
path string
want bool
}{
{pattern: "/files/", path: "/files/a/b", want: true},
{pattern: "/files/", path: "/files/", want: true},
{pattern: "/files/", path: "/files", want: false},
{pattern: "/files/", path: "/other/a", want: false},
{pattern: "/", path: "/anything/at/all", want: true},
{pattern: "/", path: "/", want: true},
// Without the trailing slash a pattern stays literal.
{pattern: "/files", path: "/files/a", want: false},
{pattern: "/files", path: "/files", want: true},
} {
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
var sm MuxSlice
sm.Reset(1)
var served bool
sm.Handle(test.pattern, func(ex *Exchange) { served = true; ex.WriteHeader(200) })
// MuxSlice must agree with the matcher it delegates to.
if ok, _ := SetPathValues(nil, test.pattern, []byte(test.path)); ok != test.want {
t.Fatalf("SetPathValues disagrees with the table: got %v", ok)
}
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: sm.MaxPathValues(),
})
conn := newConn("GET " + test.path + " HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
if !exch.Acquire(conn) {
t.Fatal("fresh exchange failed to acquire")
}
if err := Handle(exch, &sm, nopBackoff); err != nil {
t.Fatal(err)
}
if served != test.want {
t.Errorf("want served=%v, got %v", test.want, served)
}
})
}
}
// A malformed registration is a programming error, and one that otherwise costs
// a permanent silent 404 at runtime: "Get /x" parses to MethUnknown, which no
// GET request ever matches but every extension-method request does. Fail at
// registration, where the stack points at the offending line.
func TestMuxSliceHandlePanicsOnBadRegistration(t *testing.T) {
for _, test := range []struct {
name string
reg string
}{
{name: "lowercase method", reg: "Get /x"},
{name: "all lower method", reg: "get /x"},
{name: "mixed case method", reg: "pOsT /x"},
{name: "no leading slash", reg: "GET x"},
{name: "bare path no slash", reg: "x"},
{name: "empty path after method", reg: "GET "},
} {
t.Run(test.name, func(t *testing.T) {
var sm MuxSlice
sm.Reset(1)
defer func() {
if recover() == nil {
t.Errorf("want panic registering %q", test.reg)
}
}()
sm.Handle(test.reg, func(ex *Exchange) {})
})
}
}
// An exact duplicate is unreachable code: the first registration always wins.
func TestMuxSliceHandlePanicsOnDuplicate(t *testing.T) {
var sm MuxSlice
sm.Reset(2)
sm.Handle("GET /x", func(ex *Exchange) {})
defer func() {
if recover() == nil {
t.Error("want panic registering the same method and path twice")
}
}()
sm.Handle("GET /x", func(ex *Exchange) {})
}
// Extension methods are legal and uppercase, so they must still register: only
// the case-mangled forms are rejected.
func TestMuxSliceHandleAllowsExtensionMethod(t *testing.T) {
var sm MuxSlice
sm.Reset(2)
sm.Handle("PROPFIND /dav", func(ex *Exchange) {})
sm.Handle("/any-method", func(ex *Exchange) {}) // Bare path matches any method.
if sm.MaxPathValues() != 0 {
t.Errorf("want 0 path values, got %d", sm.MaxPathValues())
}
}
// pathVals sizes the slice SetPathValues writes into, so it must count exactly
// the wildcards that bind. Counting braces over-reports: "{$}" marks the path's
// end, an anonymous "{...}" has no name, and a brace inside a literal segment is
// not a wildcard at all. Each of those binds nothing.
func TestMuxSliceMaxPathValuesCountsOnlyBindingWildcards(t *testing.T) {
for _, test := range []struct {
pattern string
want int
}{
{pattern: "/health", want: 0},
{pattern: "/", want: 0},
{pattern: "/files/", want: 0}, // Anonymous trailing wildcard.
{pattern: "/{$}", want: 0}, // End-of-path marker.
{pattern: "/a/{$}", want: 0}, //
{pattern: "/b_{bucket}", want: 0}, // Literal: brace not a whole segment.
{pattern: "/{...}", want: 0}, // Multi wildcard with no name.
{pattern: "/users/{id}", want: 1}, //
{pattern: "/files/{p...}", want: 1}, //
{pattern: "/{a}/{b}", want: 2}, //
{pattern: "/b/{bucket}/o/{obj...}", want: 2},
} {
t.Run(test.pattern, func(t *testing.T) {
var sm MuxSlice
sm.Reset(1)
sm.Handle(test.pattern, func(ex *Exchange) {})
if got := sm.MaxPathValues(); got != test.want {
t.Errorf("want %d path values, got %d", test.want, got)
}
})
}
}
// Sizing and routing are different questions: "{$}" binds no values yet still
// needs the matcher, so an exact pathVals count must not send it to the literal
// comparison instead.
func TestMuxSliceZeroValueWildcardStillMatches(t *testing.T) {
for _, test := range []struct {
pattern string
path string
want bool
}{
{pattern: "/{$}", path: "/", want: true},
{pattern: "/{$}", path: "/x", want: false},
{pattern: "/a/{$}", path: "/a/", want: true},
{pattern: "/a/{$}", path: "/a/b", want: false},
{pattern: "/{...}", path: "/any/thing", want: true},
} {
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
var sm MuxSlice
sm.Reset(1)
var served bool
sm.Handle(test.pattern, func(ex *Exchange) { served = true; ex.WriteHeader(200) })
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: sm.MaxPathValues(),
})
conn := newConn("GET " + test.path + " HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
if !exch.Acquire(conn) {
t.Fatal("acquire")
}
if err := Handle(exch, &sm, nopBackoff); err != nil {
t.Fatal(err)
}
if served != test.want {
t.Errorf("want served=%v, got %v", test.want, served)
}
})
}
}
-409
View File
@@ -1,409 +0,0 @@
package httphi
import (
"errors"
"io"
"log/slog"
"math"
"sync"
"sync/atomic"
"time"
"github.com/soypat/lneto"
"github.com/soypat/lneto/internal"
)
//go:generate stringer -type Method -linecomment -output stringers.go
// reconfigureWait bounds how long [Router.Configure] waits for the previous
// generation to stop serving before reusing its exchange buffers.
const reconfigureWait = 10 * time.Millisecond
var (
errNoRequestProto = errors.New("httphi: request line with no HTTP version")
errBadRequestProto = errors.New("httphi: unsupported HTTP version in request line")
errBusyExchanges = errors.New("httphi: exchanges still serving, cannot reuse their buffers")
errRouterTornDown = errors.New("httphi: router torn down, configure it before serving")
errNotFormEncoded = errors.New("httphi: request body is not application/x-www-form-urlencoded")
errNotMultipart = errors.New("httphi: request body is not multipart/form-data")
errUnsupportedTransferCoding = errors.New("httphi: transfer coding not decoded, read the body directly")
)
type conn = io.ReadWriteCloser
// Router serves HTTP connections handed to it with [Router.Handle], routing
// each request to a handler found through its [Mux]. It plays the part of
// http.Server minus the listening: accepting connections is the caller's job,
// which is what lets the same router run over a TCP stack, a socket or a test
// pipe.
//
// A Router owns the exchanges and goroutines that serve connections and sizes
// both at [Router.Configure] time, so serving load costs no allocation and
// bounded memory. Connections arriving with nothing left to serve them are
// refused rather than queued, see [Router.Handle].
//
// Methods are safe for concurrent use. The zero value is not usable: configure
// it first.
type Router struct {
mu sync.Mutex
gen atomic.Uint32
numGoro int
reqBuf int
respBuf int
reqNumHeaderCap int
maxPathValues int
normalizeKeys bool
pendingConns chan job
mux Mux
globbuf []byte
exchs []Exchange
freeList *Exchange
log *slog.Logger
}
// job is a connection waiting on an exchange for a worker goroutine to serve it.
type job struct {
exch *Exchange
}
// RouterConfig configures a [Router]. See [Router.Configure].
//
// Each field below opens with Required, Conditional or Optional followed by the
// constraint in brackets, that being what [RouterConfig.Validate] rejects on.
type RouterConfig struct {
// Required [-1 or >0] number of goroutines to spawn on [Router.Configure],
// -1 meaning allocate them freely per connection instead.
FixedNumGoroutines int
// Required [>=32, sum with ResponseHeaderMinBufferSize <=65535] buffer for the
// request header: request-target (URI), protocol and key/value pairs.
RequestHeaderBufferSize int
// Required [>=2, <=65535] buffer for response headers. Reuses unused request
// memory so it is not a strict limit, and the status line does not count
// towards it. Once consumed [Exchange.StageHeader] appends no more fields.
ResponseHeaderMinBufferSize int
// Required [>0] request header key/value pairs to parse before failing with
// [StatusRequestHeaderFieldsTooLarge].
RequestNumHeaderKVCap int
// Optional [any] normalization of response header field keys as they are
// staged, i.e: "content-type" becomes "Content-Type".
NormalizeOutgoingKeys bool
// Conditional [>0 when FixedNumGoroutines>0, unused otherwise] depth of the
// queue connections wait in. [Router.Handle] drops connections once it is full.
MaxAwaitingConns int
// Required [non-nil] resolver of each request's method and path to the handler
// serving it. Routes must be registered before Configure, see [Mux.MaxPathValues].
Mux Mux
// Optional [nil disables] sink for failed exchanges.
Logger *slog.Logger
}
const (
// minRequestHeaderBuffer is the smallest request buffer [httpraw.Header]
// accepts with buffer growth disabled, which is how exchanges are configured.
minRequestHeaderBuffer = 32
// minResponseHeaderBuffer is the room [Exchange.FlushHeader] needs for the
// CRLF closing the header block, written even when no field was staged.
minResponseHeaderBuffer = len("\r\n")
// maxExchangeBuffer bounds an exchange's whole buffer: [Exchange] indexes it
// with uint16 offsets, so a larger one would be addressed truncated.
maxExchangeBuffer = math.MaxUint16
)
// Validate returns a non-nil error if the configuration cannot be used to
// configure a [Router].
func (cfg RouterConfig) Validate() error {
workerMode := cfg.workerMode()
switch {
case cfg.Mux == nil,
!workerMode && cfg.FixedNumGoroutines != -1,
workerMode && cfg.MaxAwaitingConns <= 0,
cfg.RequestNumHeaderKVCap <= 0,
cfg.RequestHeaderBufferSize < minRequestHeaderBuffer,
cfg.ResponseHeaderMinBufferSize < minResponseHeaderBuffer,
cfg.ResponseHeaderMinBufferSize > maxExchangeBuffer,
cfg.RequestHeaderBufferSize > maxExchangeBuffer-cfg.ResponseHeaderMinBufferSize:
return lneto.ErrInvalidConfig
}
if workerMode {
// Buffer sizes are bounded by maxExchangeBuffer above so the sum cannot
// overflow; the products below allocate and can.
exchBuf := cfg.RequestHeaderBufferSize + cfg.ResponseHeaderMinBufferSize
if cfg.FixedNumGoroutines > math.MaxInt/exchBuf ||
cfg.FixedNumGoroutines > math.MaxInt/cfg.RequestNumHeaderKVCap {
return lneto.ErrInvalidConfig
}
}
return nil
}
func (cfg RouterConfig) workerMode() bool {
return cfg.FixedNumGoroutines > 0
}
// Shutdown stops the router once in-flight exchanges finish; until
// [Router.Configure] is called again, connections are refused with a
// non-nil error. Configure calls it before installing a new generation.
func (r *Router) Shutdown() {
r.mu.Lock()
defer r.mu.Unlock()
r.shutdownLocked()
}
// shutdownLocked is [Router.Shutdown] but without locking requirement.
func (r *Router) shutdownLocked() {
r.gen.Add(1)
if r.pendingConns != nil {
close(r.pendingConns)
r.pendingConns = nil
}
}
// Configure prepares the router to serve connections, tearing down the previous
// generation of goroutines and exchanges first. In worker mode it spawns
// [RouterConfig.FixedNumGoroutines] goroutines and allocates their exchange
// buffers up front, so the router's memory use does not grow with load.
//
// Configure may be called on a serving router, but since the exchange buffers
// are reused it waits for connections in flight to finish and fails with a
// non-nil error rather than reconfigure buffers still being served from.
func (r *Router) Configure(cfg RouterConfig) error {
if err := cfg.Validate(); err != nil {
return err
}
r.mu.Lock()
defer r.mu.Unlock()
r.shutdownLocked()
gen := r.gen.Load()
numgoro := cfg.FixedNumGoroutines
workerMode := cfg.workerMode()
r.reqNumHeaderCap = cfg.RequestNumHeaderKVCap
r.reqBuf = cfg.RequestHeaderBufferSize
r.respBuf = cfg.ResponseHeaderMinBufferSize
r.mux = cfg.Mux
r.log = cfg.Logger
maxPathValues := cfg.Mux.MaxPathValues()
if maxPathValues < 0 {
return errors.New("Mux paths must be registered before configuring Router")
}
r.maxPathValues = maxPathValues
r.normalizeKeys = cfg.NormalizeOutgoingKeys
// Freelist entries were sized by the outgoing configuration: recycling one
// would serve a request with buffer limits cfg never asked for.
r.freeList = nil
if !workerMode {
r.numGoro = 0
r.pendingConns = nil
return nil
}
if workerMode {
jobqueue := make(chan job, cfg.MaxAwaitingConns)
if gen > 1 {
// Exchange buffers below are reused: the previous generation must be
// done serving before they may be handed to the new one.
err := r.awaitIdleExchangesLocked(reconfigureWait)
if err != nil {
return err
}
}
internal.SliceReuse(&r.exchs, numgoro)
r.exchs = r.exchs[:numgoro]
rawBuflen := cfg.RequestHeaderBufferSize + cfg.ResponseHeaderMinBufferSize
internal.SliceReuse(&r.globbuf, numgoro*rawBuflen)
for i := range numgoro {
// TODO exchange buffer alloc
goff := i * rawBuflen
// r.globbuf[goff:goff+rawBuflen], cfg.RequestHeaderBufferSize, cfg.RequestNumHeaderCap, cfg.NormalizeOutgoingKeys
r.exchs[i].Configure(ExchangeConfig{
RawBuf: r.globbuf[goff : goff+rawBuflen],
RequestBufferLim: cfg.RequestHeaderBufferSize,
NumHeaderKVCap: cfg.RequestNumHeaderKVCap,
NormalizeOutgoingKeys: cfg.NormalizeOutgoingKeys,
NoRequestBufferGrowth: true, // Hard memory limit.
MaxPathValues: maxPathValues,
})
go r.goroWorker(gen, jobqueue, cfg.Mux)
}
r.pendingConns = jobqueue
r.numGoro = numgoro
}
return nil
}
// awaitIdleExchangesLocked waits up to maxWait for exchanges of the previous
// generation to finish serving so their buffers may be reused. Requires r.mu
// held; the lock is released while waiting since [Router.freeExch] needs it to
// free the exchanges being waited on.
func (r *Router) awaitIdleExchangesLocked(maxWait time.Duration) error {
const pollInterval = time.Millisecond
for waited := time.Duration(0); ; waited += pollInterval {
busy := false
for i := range r.exchs {
if r.exchs[i].acquired.Load() {
busy = true
break
}
}
if !busy {
return nil
} else if waited >= maxWait {
return errBusyExchanges
}
r.mu.Unlock()
time.Sleep(pollInterval)
r.mu.Lock()
}
}
// Handle takes ownership of conn and serves one exchange on it, closing it when
// done. It does not block on the exchange: the connection is handed to a
// goroutine and Handle returns immediately.
//
// Handle returns [lneto.ErrExhausted] when no exchange is free,
// [lneto.ErrPacketDrop] when the queue of connections awaiting a goroutine is
// full, and an error when the router's goroutines have been torn down. On every
// one of them conn is left untouched and unclosed for the caller to dispose of:
// refusing connections is how a router with fixed memory applies backpressure.
func (r *Router) Handle(conn io.ReadWriteCloser) error {
// Exchange acquisition and the configuration it is served with must be read
// under the same lock: [Router.Configure] may run concurrently.
r.mu.Lock()
numGoro, mux := r.numGoro, r.mux
gen := r.gen.Load() // Generation whose buffers the exchange below is sized by.
if numGoro > 0 && r.pendingConns == nil {
// Goroutines torn down: refuse before claiming an exchange.
r.mu.Unlock()
return errRouterTornDown
}
exch := r.getExchLocked(conn)
if exch == nil {
r.mu.Unlock()
return lneto.ErrExhausted
} else if numGoro == 0 {
r.mu.Unlock()
go r.goroHandle(gen, exch, mux)
return nil
}
// Enqueue under the lock: [Router.Configure] closes pendingConns while
// holding it, so an unlocked send could land on a closed channel. The send
// never blocks, so holding the lock cannot stall a worker.
var enqueued bool
select {
case r.pendingConns <- job{exch: exch}:
enqueued = true
default:
// pendingConns cannot store another Conn, we drop and return error.
exch.acquired.Store(false) // release.
}
r.mu.Unlock()
if enqueued {
return nil
}
return lneto.ErrPacketDrop
}
func (r *Router) goroWorker(gen uint32, queue chan job, mux Mux) {
for job := range queue {
exch := job.exch
if exch == nil {
panic("httphi: unreachable nil job")
} else if gen != r.gen.Load() {
// Not released with freeExch since generation torn down,
// new buffer may have been allocated for Exchanges.
exch.Release()
continue
}
r.goroHandle(gen, exch, mux)
}
}
func (r *Router) goroHandle(gen uint32, exch *Exchange, mux Mux) {
defer r.freeExch(gen, exch)
err := Handle(exch, mux, nil)
if err != nil {
if exch.readErr != nil {
r.error("goroHandle:ReadFromLimited", slog.String("err", err.Error()))
} else {
r.error("goroHandle:TryParse?", slog.String("err", err.Error()))
}
}
}
// freeExch releases exch and offers it to the freelist for reuse. gen is the
// generation exch was acquired under: an exchange outliving its generation is
// dropped, its buffers being sized by a configuration the router no longer
// serves and possibly carved out of a globbuf it no longer owns.
func (r *Router) freeExch(gen uint32, exch *Exchange) {
const freelistMaxDepth = 5
r.mu.Lock()
if gen != r.gen.Load() {
exch.Release()
r.mu.Unlock()
return
}
depth := 0
for node := r.freeList; node != nil && depth < freelistMaxDepth; node = node.nextFree {
depth++
}
if depth < freelistMaxDepth {
// Push at head: appending at the tail would drop every node past the
// depth limit instead of dropping the exchange we cannot store.
exch.nextFree = r.freeList
r.freeList = exch
} else {
exch.nextFree = nil // Freelist full, exchange is dropped.
}
exch.Release()
r.mu.Unlock()
}
// getExchLocked returns an exchange acquired on conn. Requires r.mu held.
func (r *Router) getExchLocked(conn conn) (exch *Exchange) {
if r.freeList != nil {
// Successor must be read before Acquire: Acquire clears nextFree, so
// popping afterwards would truncate the freelist to the popped node.
next := r.freeList.nextFree
if r.freeList.Acquire(conn) {
exch = r.freeList
r.freeList = next
return exch
}
}
// Unbounded mode is stored as zero, see [Router.Configure].
workerMode := r.numGoro > 0
if workerMode {
for i := range r.exchs {
if r.exchs[i].Acquire(conn) {
return &r.exchs[i]
}
}
} else {
// Unbounded growth mode when r.numGoro==0.
exch := new(Exchange)
exch.Configure(ExchangeConfig{
RawBuf: make([]byte, r.respBuf+r.reqBuf),
RequestBufferLim: r.reqBuf,
NumHeaderKVCap: r.reqNumHeaderCap,
NormalizeOutgoingKeys: r.normalizeKeys,
NoRequestBufferGrowth: true,
MaxPathValues: r.maxPathValues,
})
exch.Acquire(conn) // Fresh exchange, CAS cannot fail.
return exch
}
return nil
}
func (r *Router) error(msg string, attrs ...slog.Attr) {
internal.LogAttrs(r.log, slog.LevelError, msg, attrs...)
}
func (r *Router) info(msg string, attrs ...slog.Attr) {
internal.LogAttrs(r.log, slog.LevelInfo, msg, attrs...)
}
-508
View File
@@ -1,508 +0,0 @@
package httphi
import (
"bytes"
"context"
"io"
"net"
"strings"
"sync"
"testing"
"time"
)
// rwconn is a in-memory conn. The router handles connections on another
// goroutine so every field is guarded; onClose lets tests await the handler.
//
// A drained rwconn reads (0,nil) as a live socket with no data pending would,
// so a partial request can be completed with AddReadable mid-handling. Tests
// that need the peer to hang up call Hangup.
type rwconn struct {
mu sync.Mutex
readable bytes.Buffer
segments []string
written bytes.Buffer
closed bool
hangup bool
failWr int
onClose chan struct{}
deadline time.Time
}
// newConn returns a conn preloaded with request and whose Close is observable
// with [rwconn.AwaitClose].
func newConn(request string) *rwconn {
r := &rwconn{onClose: make(chan struct{})}
r.AddReadable([]byte(request))
return r
}
// AddSegment queues data delivered on a later read, once everything already
// pending has been read. Models a request split over several TCP segments
// without depending on goroutine scheduling.
func (r *rwconn) AddSegment(b string) {
r.mu.Lock()
defer r.mu.Unlock()
r.segments = append(r.segments, b)
}
// FailWrites makes the next n writes fail, as a conn refusing further data
// would. Later writes succeed.
func (r *rwconn) FailWrites(n int) {
r.mu.Lock()
defer r.mu.Unlock()
r.failWr = n
}
// Hangup makes reads past the pending data return [io.EOF], as a peer that
// closed its side of the connection would.
func (r *rwconn) Hangup() {
r.mu.Lock()
defer r.mu.Unlock()
r.hangup = true
}
func (r *rwconn) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.closed {
r.closed = true
if r.onClose != nil {
close(r.onClose)
}
}
return nil
}
// AwaitClose blocks until the connection is closed by its handler or timeout elapses.
func (r *rwconn) AwaitClose(t *testing.T, timeout time.Duration) {
t.Helper()
select {
case <-r.onClose:
case <-time.After(timeout):
t.Fatal("timed out awaiting connection close by handler")
}
}
func (r *rwconn) Read(b []byte) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return 0, net.ErrClosed
} else if r.deadlineExceeded() {
return 0, context.DeadlineExceeded
} else if r.readable.Len() == 0 {
if len(r.segments) > 0 {
r.readable.WriteString(r.segments[0])
r.segments = r.segments[1:]
return r.readable.Read(b)
}
if r.hangup {
return 0, io.EOF
}
return 0, nil // No data pending, handler backs off and retries.
}
return r.readable.Read(b)
}
func (r *rwconn) Write(b []byte) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.closed {
return 0, net.ErrClosed
} else if r.deadlineExceeded() {
return 0, context.DeadlineExceeded
} else if r.failWr > 0 {
r.failWr--
return 0, io.ErrShortWrite
}
return r.written.Write(b)
}
func (r *rwconn) deadlineExceeded() bool {
return !r.deadline.IsZero() && time.Since(r.deadline) > 0
}
func (r *rwconn) AddReadable(b []byte) {
r.mu.Lock()
defer r.mu.Unlock()
r.readable.Write(b)
}
// SetDeadline makes reads and writes past t fail, as a conn with a read
// deadline set would.
func (r *rwconn) SetDeadline(t time.Time) {
r.mu.Lock()
defer r.mu.Unlock()
r.deadline = t
}
// IsClosed reports whether the connection was closed by its handler.
func (r *rwconn) IsClosed() bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.closed
}
func (r *rwconn) ViewWritten() string {
r.mu.Lock()
defer r.mu.Unlock()
return r.written.String()
}
var _ Mux = (*MuxSlice)(nil)
func configSynchronousRouter(t *testing.T, router *Router, bufferSize int, mux Mux) {
err := router.Configure(RouterConfig{
FixedNumGoroutines: -1,
Mux: mux,
RequestHeaderBufferSize: bufferSize,
RequestNumHeaderKVCap: 16,
ResponseHeaderMinBufferSize: bufferSize,
})
if err != nil {
t.Fatal(err)
}
}
func TestRouterGet(t *testing.T) {
const bufferSize = 1024
const expectResponse = "its time"
var (
sm MuxSlice
router Router
)
sm.Handle("GET /", staticPage(t, expectResponse))
configSynchronousRouter(t, &router, bufferSize, &sm)
conn := newConn("GET / HTTP/1.1\r\nHost: tinygo.org\r\n\r\n")
err := router.Handle(conn)
if err != nil {
t.Fatal(err)
}
conn.AwaitClose(t, time.Second)
got := conn.ViewWritten()
if !strings.HasPrefix(got, "HTTP/1.1 200 OK\r\n") {
t.Errorf("want 200 status line, got %q", got)
}
if !strings.HasSuffix(got, expectResponse) {
t.Errorf("want body %q at end of response, got %q", expectResponse, got)
}
}
// The handler observes the request line and header fields the router parsed.
func TestRouterRequestVisibleToHandler(t *testing.T) {
const bufferSize = 1024
var (
sm MuxSlice
router Router
)
var gotMethod, gotURI, gotHost string
var gotMethodEnum Method
sm.Handle("GET /index.html", func(ex *Exchange) {
gotMethod = string(ex.RequestMethodRaw())
gotMethodEnum = ex.RequestMethod()
gotURI = string(ex.RequestTarget())
gotHost = string(ex.RequestHeader("Host"))
ex.WriteHeader(200)
})
configSynchronousRouter(t, &router, bufferSize, &sm)
conn := newConn("GET /index.html HTTP/1.1\r\nHost: tinygo.org\r\n\r\n")
if err := router.Handle(conn); err != nil {
t.Fatal(err)
}
conn.AwaitClose(t, time.Second)
if gotMethod != "GET" {
t.Errorf("want method %q, got %q", "GET", gotMethod)
}
if gotMethodEnum != MethGet {
t.Errorf("want method enum %q, got %q", MethGet, gotMethodEnum)
}
if gotURI != "/index.html" {
t.Errorf("want URI %q, got %q", "/index.html", gotURI)
}
if gotHost != "tinygo.org" {
t.Errorf("want Host %q, got %q", "tinygo.org", gotHost)
}
}
// Router must route on method and URI, and must not invoke a handler for
// requests it has no registration for.
func TestRouterMux(t *testing.T) {
const bufferSize = 1024
for _, test := range []struct {
name string
request string
want string // Response body the matched handler must have written.
wantNoHandler bool // No registration matches: the router must answer 404 itself.
}{
{name: "get root", request: "GET / HTTP/1.1\r\nHost: h\r\n\r\n", want: "root"},
{name: "get page", request: "GET /page HTTP/1.1\r\nHost: h\r\n\r\n", want: "page"},
{name: "any method", request: "DELETE /any HTTP/1.1\r\nHost: h\r\n\r\n", want: "any"},
{name: "method mismatch", request: "POST / HTTP/1.1\r\nHost: h\r\n\r\n", wantNoHandler: true},
{name: "unknown uri", request: "GET /nowhere HTTP/1.1\r\nHost: h\r\n\r\n", wantNoHandler: true},
} {
t.Run(test.name, func(t *testing.T) {
var (
sm MuxSlice
router Router
)
// "/{$}" is the root alone: a bare "/" is a catch-all and would serve
// "root" for /page and /nowhere as well, see [SetPathValues].
sm.Handle("GET /{$}", staticPage(t, "root"))
sm.Handle("GET /page", staticPage(t, "page"))
sm.Handle("/any", staticPage(t, "any")) // No method: matches any.
configSynchronousRouter(t, &router, bufferSize, &sm)
conn := newConn(test.request)
if err := router.Handle(conn); err != nil {
t.Fatal(err)
}
conn.AwaitClose(t, time.Second)
got := conn.ViewWritten()
_, body, found := strings.Cut(got, "\r\n\r\n")
if !found {
t.Fatalf("header block never terminated: %q", got)
}
if test.wantNoHandler {
if !strings.HasPrefix(got, "HTTP/1.1 404 ") {
t.Errorf("want a 404 answer, got %q", got)
}
if body != "" {
t.Errorf("no handler must run, got body %q", body)
}
return
}
if !strings.HasPrefix(got, "HTTP/1.1 200 OK\r\n") {
t.Errorf("want a 200 answer, got %q", got)
}
if body != test.want {
t.Errorf("want body %q, got %q", test.want, body)
}
})
}
}
// A request arriving in pieces (TCP segmentation) must still be handled.
func TestRouterSplitRequest(t *testing.T) {
const bufferSize = 1024
const expectResponse = "split ok"
var (
sm MuxSlice
router Router
)
sm.Handle("GET /", staticPage(t, expectResponse))
configSynchronousRouter(t, &router, bufferSize, &sm)
conn := newConn("GET / HTTP/1.1\r\nHo")
conn.AddSegment("st: tinygo.org\r\n\r")
conn.AddSegment("\n") // Final CRLF lands in its own segment.
if err := router.Handle(conn); err != nil {
t.Fatal(err)
}
conn.AwaitClose(t, time.Second)
if got := conn.ViewWritten(); !strings.HasSuffix(got, expectResponse) {
t.Errorf("want body %q, got response %q", expectResponse, got)
}
}
func staticPage(t *testing.T, page string) HandlerFunc {
return func(ex *Exchange) {
var rw ExchangeRW // Streaming APIs take the ReadWriter view.
ex.ReadWriter(&rw)
n, err := io.WriteString(&rw, page)
// Handler runs on the router goroutine: Error, never Fatal.
if err != nil {
t.Error(err)
} else if n != len(page) {
t.Error("expected written ", len(page), "got", n)
}
}
}
// An exchange freed by the outgoing generation carries that generation's
// buffers. Recycling it under a new configuration serves the request with
// buffer limits the new [RouterConfig] never asked for.
func TestRouterReconfigureDropsStaleExchanges(t *testing.T) {
const smallBuf, largeBuf = 256, 1024
var (
sm MuxSlice
router Router
)
bufsize := make(chan int, 2)
sm.Handle("GET /", func(ex *Exchange) { bufsize <- len(ex.UnsafeRawBuffer()) })
serve := func(want int) {
t.Helper()
conn := newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
if err := router.Handle(conn); err != nil {
t.Fatal(err)
}
if got := <-bufsize; got != want {
t.Errorf("want exchange buffer %d, got %d", want, got)
}
conn.AwaitClose(t, time.Second) // Exchange hits the freelist on close.
}
configSynchronousRouter(t, &router, smallBuf, &sm)
serve(2 * smallBuf)
configSynchronousRouter(t, &router, largeBuf, &sm)
serve(2 * largeBuf)
}
// Configure writes the fields Handle reads; concurrent use must not race.
func TestRouterConfigureHandleRace(t *testing.T) {
const bufferSize = 1024
var (
sm MuxSlice
router Router
)
sm.Handle("GET /", staticPage(t, "ok"))
configSynchronousRouter(t, &router, bufferSize, &sm)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
configSynchronousRouter(t, &router, bufferSize, &sm)
}()
go func() {
defer wg.Done()
conn := newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n")
if err := router.Handle(conn); err != nil {
t.Error(err)
}
}()
wg.Wait()
}
// Reconfiguring a running router tears down the job queue that Handle may be
// sending a connection on. Connections may be dropped, but never panic.
// A torn down router has nothing left to serve with: it must say so instead of
// dropping the connection as if it were merely busy.
func TestRouterHandleAfterTeardown(t *testing.T) {
var (
sm MuxSlice
router Router
)
sm.Handle("GET /", staticPage(t, "ok"))
err := router.Configure(RouterConfig{
FixedNumGoroutines: 2,
MaxAwaitingConns: 4,
Mux: &sm,
RequestHeaderBufferSize: 512,
RequestNumHeaderKVCap: 16,
ResponseHeaderMinBufferSize: 512,
})
if err != nil {
t.Fatal(err)
}
router.Shutdown()
conn := newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n")
if err = router.Handle(conn); err != errRouterTornDown {
t.Errorf("want errRouterTornDown, got %v", err)
}
if conn.IsClosed() {
t.Error("refused connection must be left for the caller to dispose of")
}
}
// Tearing down a generation abandons the connections queued for it. They were
// taken ownership of by Handle, so they must be closed and their exchanges
// released: an exchange left claimed by a torn down generation is a buffer the
// router can never reconfigure again.
func TestRouterTeardownReleasesQueuedConns(t *testing.T) {
var (
sm MuxSlice
router Router
)
const numGoro = 2
sm.Handle("GET /", staticPage(t, "ok"))
cfg := RouterConfig{
FixedNumGoroutines: numGoro,
MaxAwaitingConns: 4,
Mux: &sm,
RequestHeaderBufferSize: 512,
RequestNumHeaderKVCap: 16,
ResponseHeaderMinBufferSize: 512,
}
// Handing connections over and tearing down immediately leaves them queued
// for workers that will never serve them. Rounds bound the scheduling luck
// needed; a single leaked exchange also fails every later Configure.
var conns [numGoro]*rwconn
for range 30 {
// errBusyExchanges is legitimate backpressure while the previous
// generation drops its connections, but it must not outlive it.
var err error
for range 100 {
if err = router.Configure(cfg); err == nil {
break
}
time.Sleep(time.Millisecond)
}
if err != nil {
t.Fatal("reconfigure after teardown:", err)
}
for i := range conns {
conns[i] = newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n")
conns[i].Hangup()
if err := router.Handle(conns[i]); err != nil {
t.Fatal(err)
}
}
router.Shutdown()
for i := range conns {
// Handle took ownership of the connection: served or dropped, the
// router closes it.
conns[i].AwaitClose(t, time.Second)
}
}
}
func TestRouterConfigureDuringWorkerHandle(t *testing.T) {
var (
sm MuxSlice
router Router
)
sm.Handle("GET /", staticPage(t, "ok"))
cfg := RouterConfig{
FixedNumGoroutines: 2,
MaxAwaitingConns: 4,
Mux: &sm,
RequestHeaderBufferSize: 512,
RequestNumHeaderKVCap: 16,
ResponseHeaderMinBufferSize: 512,
}
if err := router.Configure(cfg); err != nil {
t.Fatal(err)
}
defer router.Shutdown()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for range 300 {
conn := newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
router.Handle(conn) // Drops are fine, panics are not.
}
}()
go func() {
defer wg.Done()
// Each Configure sleeps 5ms tearing down the previous generation, keep
// the count low and let the Handle loop supply the concurrency.
for range 20 {
// errBusyExchanges is legitimate backpressure: the previous
// generation was still serving when the buffers were needed.
if err := router.Configure(cfg); err != nil && err != errBusyExchanges {
t.Error(err)
return
}
}
}()
wg.Wait()
}
-273
View File
@@ -1,273 +0,0 @@
package httphi
// StatusText returns a text for the HTTP status code. It returns the empty
// string if the code is unknown.
func StatusText(code int) string {
switch status(code) {
case StatusContinue:
return "Continue"
case StatusSwitchingProtocols:
return "Switching Protocols"
case StatusProcessing:
return "Processing"
case StatusEarlyHints:
return "Early Hints"
case StatusOK:
return "OK"
case StatusCreated:
return "Created"
case StatusAccepted:
return "Accepted"
case StatusNonAuthoritativeInfo:
return "Non-Authoritative Information"
case StatusNoContent:
return "No Content"
case StatusResetContent:
return "Reset Content"
case StatusPartialContent:
return "Partial Content"
case StatusMultiStatus:
return "Multi-Status"
case StatusAlreadyReported:
return "Already Reported"
case StatusIMUsed:
return "IM Used"
case StatusMultipleChoices:
return "Multiple Choices"
case StatusMovedPermanently:
return "Moved Permanently"
case StatusFound:
return "Found"
case StatusSeeOther:
return "See Other"
case StatusNotModified:
return "Not Modified"
case StatusUseProxy:
return "Use Proxy"
case StatusTemporaryRedirect:
return "Temporary Redirect"
case StatusPermanentRedirect:
return "Permanent Redirect"
case StatusBadRequest:
return "Bad Request"
case StatusUnauthorized:
return "Unauthorized"
case StatusPaymentRequired:
return "Payment Required"
case StatusForbidden:
return "Forbidden"
case StatusNotFound:
return "Not Found"
case StatusMethodNotAllowed:
return "Method Not Allowed"
case StatusNotAcceptable:
return "Not Acceptable"
case StatusProxyAuthRequired:
return "Proxy Authentication Required"
case StatusRequestTimeout:
return "Request Timeout"
case StatusConflict:
return "Conflict"
case StatusGone:
return "Gone"
case StatusLengthRequired:
return "Length Required"
case StatusPreconditionFailed:
return "Precondition Failed"
case StatusRequestEntityTooLarge:
return "Request Entity Too Large"
case StatusRequestURITooLong:
return "Request URI Too Long"
case StatusUnsupportedMediaType:
return "Unsupported Media Type"
case StatusRequestedRangeNotSatisfiable:
return "Requested Range Not Satisfiable"
case StatusExpectationFailed:
return "Expectation Failed"
case StatusTeapot:
return "I'm a teapot"
case StatusMisdirectedRequest:
return "Misdirected Request"
case StatusUnprocessableEntity:
return "Unprocessable Entity"
case StatusLocked:
return "Locked"
case StatusFailedDependency:
return "Failed Dependency"
case StatusTooEarly:
return "Too Early"
case StatusUpgradeRequired:
return "Upgrade Required"
case StatusPreconditionRequired:
return "Precondition Required"
case StatusTooManyRequests:
return "Too Many Requests"
case StatusRequestHeaderFieldsTooLarge:
return "Request Header Fields Too Large"
case StatusUnavailableForLegalReasons:
return "Unavailable For Legal Reasons"
case StatusInternalServerError:
return "Internal Server Error"
case StatusNotImplemented:
return "Not Implemented"
case StatusBadGateway:
return "Bad Gateway"
case StatusServiceUnavailable:
return "Service Unavailable"
case StatusGatewayTimeout:
return "Gateway Timeout"
case StatusHTTPVersionNotSupported:
return "HTTP Version Not Supported"
case StatusVariantAlsoNegotiates:
return "Variant Also Negotiates"
case StatusInsufficientStorage:
return "Insufficient Storage"
case StatusLoopDetected:
return "Loop Detected"
case StatusNotExtended:
return "Not Extended"
case StatusNetworkAuthenticationRequired:
return "Network Authentication Required"
default:
return ""
}
}
const ()
type status int
// HTTP status codes as registered with IANA.
// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
const (
// RFC 9110, 15.2.1
StatusContinue = 100 // Continue
// RFC 9110, 15.2.2
StatusSwitchingProtocols = 101 // Switching Protocols
// RFC 2518, 10.1
StatusProcessing = 102 // Processing
// RFC 8297
StatusEarlyHints = 103 // Early Hints
// RFC 9110, 15.3.1
StatusOK = 200 // OK
// RFC 9110, 15.3.2
StatusCreated = 201 // Created
// RFC 9110, 15.3.3
StatusAccepted = 202 // Accepted
// RFC 9110, 15.3.4
StatusNonAuthoritativeInfo = 203 // Non-Authoritative Information
// RFC 9110, 15.3.5
StatusNoContent = 204 // No Content
// RFC 9110, 15.3.6
StatusResetContent = 205 // Reset Content
// RFC 9110, 15.3.7
StatusPartialContent = 206 // Partial Content
// RFC 4918, 11.1
StatusMultiStatus = 207 // Multi-Status
// RFC 5842, 7.1
StatusAlreadyReported = 208 // Already Reported
// RFC 3229, 10.4.1
StatusIMUsed = 226 // IM Used
// RFC 9110, 15.4.1
StatusMultipleChoices = 300 // Multiple Choices
// RFC 9110, 15.4.2
StatusMovedPermanently = 301 // Moved Permanently
// RFC 9110, 15.4.3
StatusFound = 302 // Found
// RFC 9110, 15.4.4
StatusSeeOther = 303 // See Other
// RFC 9110, 15.4.5
StatusNotModified = 304 // Not Modified
// RFC 9110, 15.4.6
StatusUseProxy = 305 // Use Proxy
// RFC 9110, 15.4.7 (Unused)
_ = 306
// RFC 9110, 15.4.8
StatusTemporaryRedirect = 307 // Temporary Redirect
// RFC 9110, 15.4.9
StatusPermanentRedirect = 308 // Permanent Redirect
// RFC 9110, 15.5.1
StatusBadRequest = 400 // Bad Request
// RFC 9110, 15.5.2
StatusUnauthorized = 401 // Unauthorized
// RFC 9110, 15.5.3
StatusPaymentRequired = 402 // Payment Required
// RFC 9110, 15.5.4
StatusForbidden = 403 // Forbidden
// RFC 9110, 15.5.5
StatusNotFound = 404 // Not Found
// RFC 9110, 15.5.6
StatusMethodNotAllowed = 405 // Method Not Allowed
// RFC 9110, 15.5.7
StatusNotAcceptable = 406 // Not Acceptable
// RFC 9110, 15.5.8
StatusProxyAuthRequired = 407 // Proxy Authentication Required
// RFC 9110, 15.5.9
StatusRequestTimeout = 408 // Request Timeout
// RFC 9110, 15.5.10
StatusConflict = 409 // Conflict
// RFC 9110, 15.5.11
StatusGone = 410 // Gone
// RFC 9110, 15.5.12
StatusLengthRequired = 411 // Length Required
// RFC 9110, 15.5.13
StatusPreconditionFailed = 412 // Precondition Failed
// RFC 9110, 15.5.14
StatusRequestEntityTooLarge = 413 // Request Entity Too Large
// RFC 9110, 15.5.15
StatusRequestURITooLong = 414 // Request URI Too Long
// RFC 9110, 15.5.16
StatusUnsupportedMediaType = 415 // Unsupported Media Type
// RFC 9110, 15.5.17
StatusRequestedRangeNotSatisfiable = 416 // Requested Range Not Satisfiable
// RFC 9110, 15.5.18
StatusExpectationFailed = 417 // Expectation Failed
// RFC 9110, 15.5.19 (Unused)
StatusTeapot = 418 // I'm a teapot
// RFC 9110, 15.5.20
StatusMisdirectedRequest = 421 // Misdirected Request
// RFC 9110, 15.5.21
StatusUnprocessableEntity = 422 // Unprocessable Entity
// RFC 4918, 11.3
StatusLocked = 423 // Locked
// RFC 4918, 11.4
StatusFailedDependency = 424 // Failed Dependency
// RFC 8470, 5.2.
StatusTooEarly = 425 // Too Early
// RFC 9110, 15.5.22
StatusUpgradeRequired = 426 // Upgrade Required
// RFC 6585, 3
StatusPreconditionRequired = 428 // Precondition Required
// RFC 6585, 4
StatusTooManyRequests = 429 // Too Many Requests
// RFC 6585, 5
StatusRequestHeaderFieldsTooLarge = 431 // Request Header Fields Too Large
// RFC 7725, 3
StatusUnavailableForLegalReasons = 451 // Unavailable For Legal Reasons
// RFC 9110, 15.6.1
StatusInternalServerError = 500 // Internal Server Error
// RFC 9110, 15.6.2
StatusNotImplemented = 501 // Not Implemented
// RFC 9110, 15.6.3
StatusBadGateway = 502 // Bad Gateway
// RFC 9110, 15.6.4
StatusServiceUnavailable = 503 // Service Unavailable
// RFC 9110, 15.6.5
StatusGatewayTimeout = 504 // Gateway Timeout
// RFC 9110, 15.6.6
StatusHTTPVersionNotSupported = 505 // HTTP Version Not Supported
// RFC 2295, 8.1
StatusVariantAlsoNegotiates = 506 // Variant Also Negotiates
// RFC 4918, 11.5
StatusInsufficientStorage = 507 // Insufficient Storage
// RFC 5842, 7.2
StatusLoopDetected = 508 // Loop Detected
// RFC 2774, 7
StatusNotExtended = 510 // Not Extended
// RFC 6585, 6
StatusNetworkAuthenticationRequired = 511 // Network Authentication Required
)
-34
View File
@@ -1,34 +0,0 @@
// Code generated by "stringer -type Method -linecomment -output stringers.go"; DO NOT EDIT.
package httphi
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MethUndefined-0]
_ = x[MethGet-1]
_ = x[MethHead-2]
_ = x[MethPost-3]
_ = x[MethPut-4]
_ = x[MethPatch-5]
_ = x[MethDelete-6]
_ = x[MethConnect-7]
_ = x[MethOptions-8]
_ = x[MethTrace-9]
_ = x[MethUnknown-10]
}
const _Method_name = "undefinedGETHEADPOSTPUTPATCHDELETECONNECTOPTIONSTRACEunknown"
var _Method_index = [...]uint8{0, 9, 12, 16, 20, 23, 28, 34, 41, 48, 53, 60}
func (i Method) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Method_index)-1 {
return "Method(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Method_name[_Method_index[idx]:_Method_index[idx+1]]
}
+61 -43
View File
@@ -7,89 +7,105 @@ import (
// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [Header] methods.
// Cookie represents a single-line Cookie header value in a HTTP header, much like the standard library Cookie.
type Cookie struct {
kv kvBuffer
}
// EnableBufferGrowth allows the cookie's buffer to grow past what [Cookie.Reset] was
// handed. See [kvBuffer.EnableBufferGrowth].
func (c *Cookie) EnableBufferGrowth(enableBufferGrowth bool) {
c.kv.EnableBufferGrowth(enableBufferGrowth)
buf []byte
kvs []argsKV // first key-value pair is the data Key/Value pair.
}
// Reset functions very similarly to [Header.Reset]. Can be used for in-place cookie parsing.
func (c *Cookie) Reset(buf []byte, capKV int) { c.kv.Reset(buf, capKV) }
func (c *Cookie) valid() bool {
return len(c.kv.kvs) > 0 && c.kv.kvs[0].key.len > 0
func (c *Cookie) Reset(buf []byte) {
if buf == nil {
buf = c.buf[:0]
}
*c = Cookie{
buf: buf,
kvs: c.kvs[:0],
}
}
// Name returns the first cookie key which is commonly referred to as the cookie's name. Returns nil if not found.
func (c *Cookie) Name() []byte {
if !c.valid() {
if len(c.kvs) == 0 || c.kvs[0].key.len == 0 {
return nil
}
return c.kv.AtKey(0)
return tok2bytes(c.buf, c.kvs[0].key)
}
// Value returns the first cookie value associated with the name. Returns nil if not found.
func (c *Cookie) Value() []byte {
if !c.valid() {
if len(c.kvs) == 0 || c.kvs[0].value.len == 0 {
return nil
}
return c.kv.AtValue(0)
return tok2bytes(c.buf, c.kvs[0].value)
}
// ParseBytes copies the argument bytes to the Cookie's underlying buffer and parses the cookie.
func (c *Cookie) ParseBytes(cookie []byte) error {
c.Reset(nil, 0)
c.kv.buf = append(c.kv.buf[:0], cookie...)
c.Reset(nil)
c.buf = append(c.buf[:0], cookie...)
return c.Parse()
}
// CopyFrom makes a copy of the argument cookie to the receiver dst argument. No memory is shared between cookies.
func (dst *Cookie) CopyFrom(c Cookie) { dst.kv.CopyFrom(&c.kv) }
func (dst *Cookie) CopyFrom(c Cookie) {
dst.buf = append(dst.buf[:0], c.buf...)
dst.kvs = append(dst.kvs[:0], c.kvs...)
}
// Parse parses the cookie's buffer in place.
func (c *Cookie) Parse() error {
if c.kv.Len() > 0 {
if len(c.kvs) > 0 {
return errCookiesParsed
}
off := 0
for {
k, v, n := parseCookie(c.kv.buf[off:])
k, v, n := parseCookie(c.buf[off:])
if n == 0 {
break
}
if !c.kv.setInternal(k, v) {
return ErrBufferExhausted
}
c.kvs = append(c.kvs, argsKV{
key: bytes2tok(c.buf, k),
value: bytes2tok(c.buf, v),
})
off += n
}
if c.kv.Len() == 0 {
if len(c.kvs) == 0 {
return errNoCookies
}
return nil
}
// ForEach iterates over the cookie's key-value pairs, stopping on the first
// error returned by cb and returning it.
func (c *Cookie) ForEach(cb func(key, value []byte) bool) {
c.kv.ForEach(cb)
func (c *Cookie) ForEach(cb func(key, value []byte) error) error {
nc := len(c.kvs)
for i := range nc {
kv := c.kvs[i]
key := tok2bytes(c.buf, kv.key)
value := tok2bytes(c.buf, kv.value)
err := cb(key, value)
if err != nil {
return err
}
}
return nil
}
// Get gets a cookie's value from its key. Use HasValueOrKey to check if a key or single-valued cookie is present in the cookie.
func (c *Cookie) Get(key string) []byte { return c.kv.Get(key) }
func (c *Cookie) Get(key string) []byte {
nc := len(c.kvs)
for i := range nc {
kv := c.kvs[i]
if b2s(tok2bytes(c.buf, kv.key)) == key {
return tok2bytes(c.buf, kv.value)
}
}
return nil
}
// HasKeyOrSingleValue returns true if the cookie contains a pair with the given
// key or a valueless attribute with the given text, i.e: "Secure" or "HttpOnly".
// It cannot defer to [KVBuffer.Present]: parseCookie stores a valueless
// attribute with an empty key and the text as the value, so a key-only lookup
// would never match one.
func (c *Cookie) HasKeyOrSingleValue(keyOrSingleValue string) bool {
for i, nc := 0, c.kv.Len(); i < nc; i++ {
k, v := c.kv.At(i)
if (len(k) == 0 && b2s(v) == keyOrSingleValue) || b2s(k) == keyOrSingleValue {
nc := len(c.kvs)
for i := range nc {
kv := c.kvs[i]
if kv.key.len == 0 && b2s(tok2bytes(c.buf, kv.value)) == keyOrSingleValue ||
b2s(tok2bytes(c.buf, kv.key)) == keyOrSingleValue {
return true
}
}
@@ -142,14 +158,16 @@ func (c *Cookie) String() string {
// AppendKeyValues appends the HTTP header value of the cookie expected after the "Cookie:" string. Does not include trailing \r\n's.
func (c *Cookie) AppendKeyValues(dst []byte) []byte {
nc := c.kv.Len()
nc := len(c.kvs)
for i := range nc {
k, v := c.kv.At(i)
if len(k) != 0 {
dst = append(dst, k...)
kv := c.kvs[i]
key := tok2bytes(c.buf, kv.key)
value := tok2bytes(c.buf, kv.value)
if len(key) != 0 {
dst = append(dst, key...)
dst = append(dst, '=')
}
dst = append(dst, v...)
dst = append(dst, value...)
if i+1 < nc {
dst = append(dst, ';', ' ')
}
-162
View File
@@ -1,162 +0,0 @@
package httpraw
import (
"bytes"
"io"
)
// Form holds "application/x-www-form-urlencoded" key-value pairs, the encoding
// HTML forms use for POST bodies and query strings alike. Methods function
// similarly to eponymous [Cookie] methods.
//
// Pairs are stored as they appear on the wire, percent-encoded and with '+'
// undecoded, until [Form.Decode] rewrites them in place. The caller bounds the
// data: Form parses the buffer it is handed and reads nothing more.
type Form struct {
kv kvBuffer
}
// EnableBufferGrowth allows the form's buffer to grow past what [Form.Reset] was
// handed. See [kvBuffer.EnableBufferGrowth].
func (f *Form) EnableBufferGrowth(enableGrowth bool) { f.kv.EnableBufferGrowth(enableGrowth) }
// Reset discards parsed pairs and sets the buffer to parse in place.
// If buf is nil the current buffer is reused.
//
// capKV sizes the pair table. With growth disabled it is a hard limit, so a
// capKV of 0 leaves no room for a single pair and [Form.Parse] answers
// [ErrBufferExhausted]; size it to the pairs expected.
func (f *Form) Reset(buf []byte, capKV int) {
f.kv.Reset(buf, capKV)
}
// ReadFromBytes appends buf to the underlying buffer, accumulating data to parse. Returns ErrBufferExhausted when buf does not fit and growth is disabled.
func (f *Form) ReadFromBytes(b []byte) error { return f.kv.ReadFromBytes(b) }
// BufferUsed returns bytes accumulated by the Read* methods and awaiting a
// [Form.Parse]. See [kvBuffer.BufferUsed].
func (f *Form) BufferUsed() int { return f.kv.BufferUsed() }
// ReadLimited appends at most limit bytes read from r to the underlying buffer. A read returning data alongside io.EOF reports a nil error, later ones io.EOF.
func (f *Form) ReadLimited(r io.Reader, limit int) (int, error) { return f.kv.ReadLimited(r, limit) }
// ParseBytes copies the argument bytes to the Form's underlying buffer and parses them.
func (f *Form) ParseBytes(b []byte) error {
f.Reset(nil, 0)
if len(b) == 0 {
return nil // An empty body is an empty form, not a failure to read one.
}
err := f.kv.ReadFromBytes(b)
if err != nil {
return err
}
return f.Parse()
}
// Parse parses the form's buffer in place.
func (f *Form) Parse() error {
f.kv.discardKVs()
key, value, rest := NextQueryPair(f.kv.buf)
for key != nil {
if !f.kv.setInternal(key, value) {
return ErrBufferExhausted
}
key, value, rest = NextQueryPair(rest)
}
return nil
}
// Decode rewrites every key and value in place, replacing percent escapes and
// '+' with the bytes they encode. Decoding only shrinks, so no memory is added.
func (f *Form) Decode() error {
const plusAsSpace = true // Form encoded data, unlike a path.
nkvs := f.kv.Len()
for i := range nkvs {
k, v := f.kv.At(i)
nk, err := CopyDecodedPercentURL(k, k, plusAsSpace)
if err != nil {
return err
} else if len(v) == 0 {
if nk != len(k) {
f.kv.setAt(i, k[:nk], v) // k[:nk]: the decoded key is shorter.
}
continue
}
nv, err := CopyDecodedPercentURL(v, v, plusAsSpace)
if err != nil {
return err
}
if nk != len(k) || nv != len(v) {
f.kv.setAt(i, k[:nk], v[:nv])
}
}
return nil
}
// Len returns the amount of key-value pairs parsed.
func (f *Form) Len() int { return f.kv.Len() }
// Pair returns the i'th key-value pair in wire order. The value is nil for a
// pair with no '=', i.e: "ok" in "ok&q=go", which distinguishes it from "ok="
// where the value is present and empty.
func (f *Form) Pair(i int) (key, value []byte) {
return f.kv.At(i)
}
// Get returns the value of the first pair matching key, nil if absent or if the
// pair has no value. Bytes are compared as stored, so call [Form.Decode] first
// when keys may be encoded.
func (f *Form) Get(key string) []byte { return f.kv.Get(key) }
// Has returns true if key is present, with or without a value.
func (f *Form) Has(key string) bool { return f.kv.Present(key) }
// AppendKeyValues appends the form's wire representation to dst and returns it.
func (f *Form) AppendKeyValues(dst []byte) []byte {
nkv := f.kv.Len()
for i := range nkv {
key, value := f.Pair(i)
if i > 0 {
dst = append(dst, '&')
}
dst = append(dst, key...)
if value != nil {
dst = append(dst, '=')
dst = append(dst, value...)
}
}
return dst
}
// NextQueryPair splits the leading key-value pair off a query string and returns
// what remains of it. Loop until rawkey is nil:
//
// rawkey, rawval, rest := httpraw.NextQueryPair(h.RequestQuery())
// for rawkey != nil {
// // use rawkey, rawval.
// rawkey, rawval, rest = httpraw.NextQueryPair(rest)
// }
//
// A pair with no '=' yields a nil rawval, i.e: "debug" in "?debug&q=go", which
// distinguishes it from "?debug=" where the value is present and empty. Empty
// sequences are skipped, so "?&&q=go&" yields a single pair. Only '&' separates
// pairs and only the first '=' splits a pair.
func NextQueryPair(query []byte) (rawkey, rawval, rest []byte) {
for len(query) > 0 {
pair := query
amp := bytes.IndexByte(query, '&')
if amp >= 0 {
pair, query = query[:amp], query[amp+1:]
} else {
query = nil
}
if len(pair) == 0 {
continue // Empty sequence, see WHATWG URL urlencoded parsing.
}
if before, after, ok := bytes.Cut(pair, []byte{'='}); ok {
return before, after, query
}
return pair, nil, query
}
return nil, nil, nil
}
-198
View File
@@ -1,198 +0,0 @@
package httpraw
import (
"strings"
"testing"
)
// render joins a form's pairs as "key=value", a valueless key as "key".
func render(f *Form) string {
var sb strings.Builder
for i := range f.Len() {
key, value := f.Pair(i)
if i > 0 {
sb.WriteByte('|')
}
sb.Write(key)
if value != nil {
sb.WriteByte('=')
sb.Write(value)
}
}
return sb.String()
}
func TestFormParse(t *testing.T) {
for _, test := range []struct {
body string
want string
}{
{body: "", want: ""},
{body: "q=go", want: "q=go"},
{body: "name=Jos%C3%A9+P%C3%A9rez&msg=hi+there&ok=on", want: "name=Jos%C3%A9+P%C3%A9rez|msg=hi+there|ok=on"},
{body: "ok", want: "ok"}, // Flag: no '=' at all.
{body: "ok=", want: "ok="}, // Present but empty.
{body: "&&q=go&", want: "q=go"}, // Empty sequences skipped.
{body: "tag=a&tag=b", want: "tag=a|tag=b"}, // Duplicates kept in order.
{body: "=v", want: "=v"}, // Empty name kept.
{body: "a=b=c", want: "a=b=c"}, // Only the first '=' splits.
} {
var f Form
if err := f.ParseBytes([]byte(test.body)); err != nil {
t.Fatalf("%q: %s", test.body, err)
}
if got := render(&f); got != test.want {
t.Errorf("%q: want %q, got %q", test.body, test.want, got)
}
}
}
// Decode rewrites keys and values in place: percent escapes and '+' as space.
func TestFormDecode(t *testing.T) {
var f Form
const body = "name=Jos%C3%A9+P%C3%A9rez&a%20b=c%2Bd&ok"
if err := f.ParseBytes([]byte(body)); err != nil {
t.Fatal(err)
}
if err := f.Decode(); err != nil {
t.Fatal(err)
}
const want = "name=José Pérez|a b=c+d|ok"
if got := render(&f); got != want {
t.Errorf("want %q, got %q", want, got)
}
if got := string(f.Get("a b")); got != "c+d" {
t.Errorf("want decoded key lookup %q, got %q", "c+d", got)
}
}
// Decoding a valueless key that shrinks must rewrite the key without inventing
// a value: the pair has no '=' before Decode and must have none after.
func TestFormDecodeValuelessKeyShrinks(t *testing.T) {
var f Form
const body = "a=1&o%6Bay"
if err := f.ParseBytes([]byte(body)); err != nil {
t.Fatal(err)
}
if err := f.Decode(); err != nil {
t.Fatal(err)
}
const want = "a=1|okay"
if got := render(&f); got != want {
t.Errorf("want %q, got %q", want, got)
}
if _, value := f.Pair(1); value != nil {
t.Errorf("want valueless pair to stay valueless, got value %q", value)
}
if !f.Has("okay") {
t.Error("want decoded valueless key present")
}
}
// A malformed escape must be reported, never silently passed through.
func TestFormDecodeMalformed(t *testing.T) {
for _, body := range []string{"q=%zz", "%zz=v", "q=%4"} {
var f Form
if err := f.ParseBytes([]byte(body)); err != nil {
t.Fatal(err)
}
if err := f.Decode(); err == nil {
t.Errorf("%q: want decode error, got nil", body)
}
}
}
func TestFormGetHas(t *testing.T) {
var f Form
if err := f.ParseBytes([]byte("tag=a&tag=b&ok&empty=")); err != nil {
t.Fatal(err)
}
if got := string(f.Get("tag")); got != "a" {
t.Errorf("want first value %q, got %q", "a", got)
}
if got := f.Get("ok"); got != nil {
t.Errorf("want nil value for valueless key, got %q", got)
}
if got := f.Get("nope"); got != nil {
t.Errorf("want nil for absent key, got %q", got)
}
if v := f.Get("empty"); v == nil || len(v) != 0 {
t.Errorf("want present empty value, got %v", v)
}
for _, key := range []string{"tag", "ok", "empty"} {
if !f.Has(key) {
t.Errorf("want Has(%q) true", key)
}
}
if f.Has("nope") {
t.Error("want Has(nope) false")
}
}
func TestFormAppendKeyValues(t *testing.T) {
const body = "name=go&ok&empty=&tag=a&tag=b"
var f Form
if err := f.ParseBytes([]byte(body)); err != nil {
t.Fatal(err)
}
if got := string(f.AppendKeyValues(nil)); got != body {
t.Errorf("want round trip %q, got %q", body, got)
}
}
// Parsing into a reused Form must not allocate: the pair storage is reused.
func TestFormParseReuseNoAlloc(t *testing.T) {
body := []byte("name=go&tag=a&tag=b&ok")
var f Form
if err := f.ParseBytes(body); err != nil { // Warm up the pair storage.
t.Fatal(err)
}
allocs := testing.AllocsPerRun(100, func() {
f.Reset(body, 0) // 0 preserves the pair storage warmed up above, the reuse under test.
f.Parse()
})
if allocs != 0 {
t.Errorf("reused Form allocated %v times, want 0", allocs)
}
}
// BufferUsed reports buffered bytes, not parsed pairs, so a caller appending
// from several sources can tell whether a separator is needed before the next
// one. Form.Len is zero until Parse runs and cannot answer that.
func TestFormBufferUsed(t *testing.T) {
var f Form
f.Reset(nil, defaultKVCap)
if got := f.BufferUsed(); got != 0 {
t.Errorf("want 0 on a fresh form, got %d", got)
}
if err := f.ReadFromBytes([]byte("a=1")); err != nil {
t.Fatal(err)
}
if got := f.BufferUsed(); got != 3 {
t.Errorf("want 3 buffered, got %d", got)
}
if got := f.Len(); got != 0 {
t.Errorf("Len must stay 0 until Parse, got %d", got)
}
// A second source appended behind a separator.
if err := f.ReadFromBytes([]byte("&b=2")); err != nil {
t.Fatal(err)
}
if got := f.BufferUsed(); got != 7 {
t.Errorf("want 7 buffered, got %d", got)
}
if err := f.Parse(); err != nil {
t.Fatal(err)
}
if got := render(&f); got != "a=1|b=2" {
t.Errorf("want a=1|b=2, got %q", got)
}
if got := f.BufferUsed(); got != 7 {
t.Errorf("BufferUsed must not change on Parse, got %d", got)
}
// Reset discards the pairs and the buffered bytes with them.
f.Reset(nil, defaultKVCap)
if got := f.BufferUsed(); got != 0 {
t.Errorf("want 0 after Reset, got %d", got)
}
}
+182 -275
View File
@@ -3,40 +3,31 @@ package httpraw
import (
"bytes"
"io"
"strconv"
"slices"
)
const (
methodGet = "GET"
strHTTP11 = "HTTP/1.1"
strCRLF = "\r\n"
headerCookie = "Cookie"
headerConnection = "Connection"
headerContentLength = "Content-Length"
strClose = "close"
strKeepAlive = "keep-alive"
methodGet = "GET"
strHTTP11 = "HTTP/1.1"
strCRLF = "\r\n"
headerCookie = "Cookie"
headerConnection = "Connection"
strClose = "close"
)
// Flags is a bitset of signals gathered while parsing or building a header,
// such as a status code having been set or the peer requesting connection
// close. See [Header.Flags].
type Flags uint16
type flags uint16
const (
flagNoBufferGrow Flags = 1 << iota
flagNoBufferGrow flags = 1 << iota
flagDoneParsingHeader
flagOOMReached
flagConnClose
flagNoHTTP11
flagMangledBuffer // set when header fields appended to buffer via Add,Set calls
flagKVAppended // set after KV appended to buffer outside Read methods.
flagReaderEOF
// set if [Header.SetStatus] or [Header.SetStatusInt] has been called.
FlagStatusSet
)
// HasAny returns true if any of the argument flags are set.
func (f Flags) HasAny(checkThese Flags) bool {
func (f flags) hasAny(checkThese flags) bool {
return f&checkThese != 0
}
@@ -51,34 +42,32 @@ type Header struct {
hbuf headerBuf
// Request fields.
method view
requestTarget view
proto view
method headerSlice
requestURI headerSlice
proto headerSlice
// Response fields.
statusCode view
statusText view
_ noCopy
statusCode headerSlice
statusText headerSlice
flags flags
_ noCopy
}
// Flags returns [Flags] to signal status code has been set, Connection:Close or other useful signals provided by flags.
func (h *Header) Flags() Flags { return h.hbuf.kv.flags }
// ConfigBufferGrowth configures the memory the header may use. Setting
// outlives [Header.Reset]. Call before parsing/reading.
//
// enableBufferGrowth enables growing both the header buffer and the header key/value pair slice.
func (h *Header) ConfigBufferGrowth(enableBufferGrowth bool) {
h.hbuf.kv.EnableBufferGrowth(enableBufferGrowth)
// EnableBufferGrowth disables buffer growth during parsing if b is false. Is enabled by default.
// Disabling buffer growth prevents allocations but methods may throw errors on insufficient memory.
func (h *Header) EnableBufferGrowth(b bool) {
if !b {
h.flags |= flagNoBufferGrow
} else {
h.flags &^= flagNoBufferGrow
}
}
// ParseBytes copies the bytes into buffer and parses the HTTP header. It fails if HTTP header data is incomplete.
func (h *Header) ParseBytes(asResponse bool, b []byte) error {
h.Reset(nil, 0)
err := h.hbuf.kv.ReadFromBytes(b)
if err != nil {
return err
}
h.Reset(nil)
h.hbuf.readFromBytes(b)
return h.parse(asResponse)
}
@@ -86,7 +75,7 @@ func (h *Header) ParseBytes(asResponse bool, b []byte) error {
// It fails if HTTP data is incomplete.
func (h *Header) Parse(asResponse bool) error {
debuglog("http:parse:reset")
h.Reset(h.hbuf.kv.buf, 0)
h.Reset(h.hbuf.buf)
debuglog("http:parse:start")
return h.parse(asResponse)
}
@@ -108,68 +97,96 @@ func (h *Header) Parse(asResponse bool) error {
// return err
// }
func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
flags := h.Flags()
if flags.HasAny(flagDoneParsingHeader) {
if h.flags.hasAny(flagDoneParsingHeader) {
return false, errAlreadyParsed
} else if flags.HasAny(flagMangledBuffer) {
} else if h.flags.hasAny(flagMangledBuffer) {
return false, errMangledBuffer
}
if asResponse && h.statusCode.len == 0 || !asResponse && h.requestTarget.start == 0 {
if asResponse && h.statusCode.len == 0 || !asResponse && h.requestURI.start == 0 {
err = h.parseFirstLine(asResponse)
if err != nil {
return err == ErrNeedMoreData, err
return err == errNeedMore, err
}
}
err = h.parseNextHeaders(flags)
return err == ErrNeedMoreData, err
err = h.parseNextHeaders()
return err == errNeedMore, err
}
// ParsingSuccess returns true if TryParse was successful, that is to say it returned needMoreData==false and err==nil.
func (h *Header) ParsingSuccess() bool {
return h.Flags().HasAny(flagDoneParsingHeader)
return h.flags.hasAny(flagDoneParsingHeader)
}
// ReadFromLimited reads at most maxBytesToRead from reader and appends them to underlying buffer.
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
// If read is successful (read length>0) and reader returns [io.EOF] then ReadFromLimited will return a nil error.
func (h *Header) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) {
return h.hbuf.kv.ReadLimited(r, maxBytesToRead)
if maxBytesToRead <= 0 {
return 0, errSmallBuffer
} else if h.flags.hasAny(flagMangledBuffer) {
return 0, errMangledBuffer
}
free := h.BufferFree()
if free < maxBytesToRead {
if h.flags.hasAny(flagNoBufferGrow) {
return 0, errSmallBuffer
}
h.hbuf.buf = slices.Grow(h.hbuf.buf, maxBytesToRead)
}
blen := len(h.hbuf.buf)
b := h.hbuf.buf[blen:min(blen+maxBytesToRead, cap(h.hbuf.buf))]
n, err := r.Read(b)
if err != nil && err == io.EOF {
h.flags |= flagReaderEOF
if n > 0 {
err = nil // Nil-out error if read was succesful so as to not spook readers.
}
}
h.hbuf.buf = h.hbuf.buf[:blen+n]
return n, err
}
// ReadFromBytes appends argument buffer to underlying buffer.
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
func (h *Header) ReadFromBytes(b []byte) error {
return h.hbuf.kv.ReadFromBytes(b)
func (h *Header) ReadFromBytes(b []byte) (int, error) {
if len(b) == 0 {
return 0, errSmallBuffer
}
free := h.BufferFree()
if free < len(b) {
if h.flags.hasAny(flagNoBufferGrow) {
return 0, errSmallBuffer
}
h.hbuf.buf = slices.Grow(h.hbuf.buf, len(b))
}
h.hbuf.readFromBytes(b)
return len(b), nil
}
// BufferReceived returns the amoung of bytes read during calls to Read* methods.
// Returns 0 if buffer is invalid/mangled.
func (h *Header) BufferReceived() int {
if h.Flags().HasAny(flagMangledBuffer | flagOOMReached) {
if h.flags.hasAny(flagMangledBuffer | flagOOMReached) {
return 0
}
return len(h.hbuf.kv.BufferRaw())
return len(h.hbuf.buf)
}
// BufferParsed returns the amount of bytes parsed during a call to Parse* methods.
// If the Parse* method completed without error then BufferParsed returns the header's length including the final "\r\n\r\n" text.
// BufferParsed returns 0 if the buffer is invalid/mangled or if no header data has been parsed succesfully.
func (h *Header) BufferParsed() int {
if h.Flags().HasAny(flagMangledBuffer | flagOOMReached) {
if h.flags.hasAny(flagMangledBuffer | flagOOMReached) {
return 0
}
return h.hbuf.off
}
// BufferRaw returns the undeerlying buffer as stored currently in memory.
// The length of the returned buffer is the used portion. Capacity of returned slice is [Header.BufferCapacity].
func (h *Header) BufferRaw() []byte { return h.hbuf.kv.BufferRaw() }
// BufferUsed returns the raw memory used.
//
// BufferUsed + BufferFree == BufferCapacity
func (h *Header) BufferUsed() int {
return len(h.hbuf.kv.BufferRaw())
return len(h.hbuf.buf)
}
// BufferFree returns amount of bytes free in underlying buffer.
@@ -183,12 +200,29 @@ func (h *Header) BufferFree() int {
//
// BufferUsed + BufferFree == BufferCapacity
func (h *Header) BufferCapacity() int {
return cap(h.hbuf.kv.BufferRaw())
return cap(h.hbuf.buf)
}
// ForEach iterates over header key-value field tuples.
func (h *Header) ForEach(cb func(key, value []byte) bool) {
h.hbuf.kv.ForEach(cb)
func (h *Header) ForEach(cb func(key, value []byte) error) error {
return h.hbuf.forEach(cb)
}
func (hb *headerBuf) forEach(cb func(key, value []byte) error) error {
nh := len(hb.headers)
for i := range nh {
kv := hb.headers[i]
if !kv.isValid() {
continue
}
key := hb.musttoken(kv.key)
value := hb.musttoken(kv.value)
err := cb(key, value)
if err != nil {
return err
}
}
return nil
}
// Reset discards all parsed data and sets the buffer data to buf. This method
@@ -196,28 +230,30 @@ func (h *Header) ForEach(cb func(key, value []byte) bool) {
// data with Reset to parse data in-place.
// If buf is nil then the current buffer is reused. There are 3 ways to use Reset:
//
// h.Reset(prealloc[:0], 16); h.ParseBytes(httpHeader) // Tell header to use a pre-allocated buffer capacity.
// h.Reset(httpHeader, 16); h.Parse() // Parse bytes in place with no copying.
// h.Reset(prealloc[:0]); h.ParseBytes(httpHeader) // Tell header to use a pre-allocated buffer capacity.
// h.Reset(httpHeader); h.Parse() // Parse bytes in place with no copying.
// h.Reset(nil) // Reuse buffer previously set in a call to Reset.
func (h *Header) Reset(buf []byte, numHeaderCapacity int) {
const persistentFlags = flagNoBufferGrow
debuglog("http:reset:hbuf")
h.hbuf.reset(buf, numHeaderCapacity)
if h.Flags().HasAny(flagNoBufferGrow) && h.BufferCapacity() < 32 {
func (h *Header) Reset(buf []byte) {
if h.flags.hasAny(flagNoBufferGrow) && cap(buf) < 32 {
panic("small buffer and flagNoBufferGrow set")
}
*h = Header{hbuf: h.hbuf}
const persistentFlags = flagNoBufferGrow
debuglog("http:reset:hbuf")
h.hbuf.reset(buf)
*h = Header{
hbuf: h.hbuf,
flags: h.flags & persistentFlags,
}
debuglog("http:reset:done")
}
// Body returns the surplus data following headers. It is only valid as long as Parse* or Reset methods are not called.
func (h *Header) Body() ([]byte, error) {
debuglog("http:body")
flags := h.Flags()
if flags.HasAny(flagMangledBuffer) {
if h.flags.hasAny(flagMangledBuffer) {
return nil, errMangledBuffer
} else if flags.HasAny(flagDoneParsingHeader) {
return h.BufferRaw()[h.hbuf.off:], nil
} else if h.flags.hasAny(flagDoneParsingHeader) {
return h.hbuf.buf[h.hbuf.off:], nil
}
return nil, errUnparsed
}
@@ -235,56 +271,63 @@ func (h *Header) SetInt(key string, value int64, base int) {
if base < 2 || base > 36 {
return // strconv.AppendInt only supports base 2..36.
}
h.hbuf.kv.SetInt(key, value, base)
useKv := h.takeReusableSlot(key)
if useKv == nil {
h.appendHeaderInt(key, value, base)
} else {
useKv.value = h.reuseOrAppendInt(useKv.value, value, base)
}
}
// Set sets a key-value pair in the HTTP header.
// Calling Set mangles the buffer.
func (h *Header) Set(key, value string) (enoughSpace bool) {
return h.hbuf.kv.Set(key, value)
func (h *Header) Set(key, value string) {
useKv := h.takeReusableSlot(key)
if useKv == nil {
h.appendHeader(key, value)
} else {
useKv.value = h.reuseOrAppend(useKv.value, value)
}
}
// Get gets the first exact-match value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key.
func (h *Header) Get(key string) []byte {
return h.hbuf.kv.Get(key)
}
// GetFold gets the first value whose key matches key under ASCII case-insensitive
// comparison, i.e: "content-length" matches "Content-Length".
// Use [Header.Get] for exact match and [Header.ForEach] to find multiple values
// corresponding to same key.
func (h *Header) GetFold(key string) []byte {
return h.hbuf.kv.GetFold(key)
}
// NormalizeKeys normalizes all header keys. i.e: CONTENT-type -> Content-Type
func (h *Header) NormalizeKeys() {
for i, kv := range h.hbuf.kv.kvs {
if kv.isValidHeader() {
NormalizeHeaderKey(h.hbuf.kv.AtKey(i))
// takeReusableSlot returns the valid key-value entry for key with the largest
// value buffer (best candidate for in-place reuse) and invalidates any other
// entries sharing the key. Returns nil if the key is not present.
func (h *Header) takeReusableSlot(key string) *argsKV {
hb := &h.hbuf
var useKv *argsKV
for i := 0; i < len(hb.headers); i++ {
// Search for key-value with largest buffer for value to store value reusing buffer.
gotkv := &hb.headers[i]
if gotkv.isValid() && b2s(hb.musttoken(gotkv.key)) == key {
if useKv == nil {
useKv = gotkv
} else if gotkv.value.len > useKv.value.len {
useKv.invalidate()
useKv = gotkv
} else {
gotkv.invalidate()
}
}
}
return useKv
}
// ContentLength returns the body length declared by the Content-Length field.
// If the field is not present then the returned bool is false. Will return error for invalid or non-integer value.
func (h *Header) ContentLength() (_ int64, present bool, _ error) {
value := h.GetFold(headerContentLength)
if value == nil {
return 0, false, nil
// Get gets the first value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key.
func (h *Header) Get(key string) []byte {
debuglog("http:get:start")
kv := h.peekHeader(key)
if kv.isValid() {
debuglog("http:get:found")
return h.hbuf.musttoken(kv.value)
}
value = trimOWS(value)
// Unsigned parse of 63 bits rejects a sign and anything past int64's range.
n, err := strconv.ParseInt(b2s(value), 10, 64)
if err != nil || n < 0 {
return n, true, errBadContentLength // strconv's error allocates and is not comparable.
}
return n, true, nil
debuglog("http:get:notfound")
return nil
}
// Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer.
func (h *Header) Add(key, value string) {
h.hbuf.kv.appendPair(key, value)
h.appendHeader(key, value)
}
// Method returns HTTP request method.
@@ -294,41 +337,17 @@ func (h *Header) Method() []byte {
// SetMethod sets the request header's method.
func (h *Header) SetMethod(method string) {
h.method = h.hbuf.kv.reuseOrAppend(h.method, method)
h.method = h.reuseOrAppend(h.method, method)
}
// SetRequestTarget sets request-target (URI) for the first HTTP request line.
func (h *Header) SetRequestTarget(requestTarget string) {
h.requestTarget = h.hbuf.kv.reuseOrAppend(h.requestTarget, requestTarget)
// SetRequestURI sets RequestURI for the first HTTP request line.
func (h *Header) SetRequestURI(requestURI string) {
h.requestURI = h.reuseOrAppend(h.requestURI, requestURI)
}
// RequestTarget returns a view of the request-target (URI) of the first HTTP request line.
// Called Request-URI in the obsolete RFC 2616, renamed request-target by RFC 9112.
func (h *Header) RequestTarget() []byte {
return h.getNonEmptyValue(h.requestTarget)
}
// RequestPath returns the request-target (URI) up to the query string, i.e: "/search"
// for "/search?q=go". Returns the whole target if it contains no query string.
func (h *Header) RequestPath() []byte {
target := h.RequestTarget()
before, _, ok := bytes.Cut(target, []byte{'?'})
if !ok {
return target
}
return before
}
// RequestQuery returns the request-target (URI) query string as it appears on the
// wire, percent-encoded and with '+' undecoded, i.e: "q=go" for "/search?q=go".
// Returns nil if the target has no query string. Iterate it with [NextQueryPair].
func (h *Header) RequestQuery() []byte {
target := h.RequestTarget()
_, after, ok := bytes.Cut(target, []byte{'?'})
if !ok {
return nil
}
return after
// RequestURI returns RequestURI from the first HTTP request line.
func (h *Header) RequestURI() []byte {
return h.getNonEmptyValue(h.requestURI)
}
// Protocol returns the request header's HTTP protocol. Usually "HTTP/1.1".
@@ -338,7 +357,7 @@ func (h *Header) Protocol() []byte {
// SetProtocol sets the request header's protocol. Usually "HTTP/1.1".
func (h *Header) SetProtocol(protocol string) {
h.proto = h.hbuf.kv.reuseOrAppend(h.proto, protocol)
h.proto = h.reuseOrAppend(h.proto, protocol)
}
// Status returns the response header's status code and status text. i.e: "200" "OK".
@@ -346,36 +365,28 @@ func (h *Header) Status() (code, statusText []byte) {
if h.statusCode.len == 0 {
return nil, nil
}
return h.hbuf.kv.musttoken(h.statusCode), h.hbuf.kv.musttoken(h.statusText)
return h.hbuf.musttoken(h.statusCode), h.hbuf.musttoken(h.statusText)
}
// SetStatus sets the response header's status code and status text. i.e: "200" "OK".
// Status sets the response header's status code and status text. i.e: "200" "OK".
func (h *Header) SetStatus(code, statusText string) {
h.hbuf.kv.flags |= FlagStatusSet
h.statusCode = h.hbuf.kv.reuseOrAppend(h.statusCode, code)
h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText)
h.statusCode = h.reuseOrAppend(h.statusCode, code)
h.statusText = h.reuseOrAppend(h.statusText, statusText)
}
// SetStatusInt is identical to [Header.SetStatus] but performs integer to text conversion for status code.
func (h *Header) SetStatusInt(code int64, statusText string) {
h.hbuf.kv.flags |= FlagStatusSet
h.statusCode = h.hbuf.kv.reuseOrAppendInt(h.statusCode, code, 10)
h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText)
}
func (h *Header) getNonEmptyValue(s view) []byte {
func (h *Header) getNonEmptyValue(s headerSlice) []byte {
if s.len == 0 {
return nil // If empty then value is invalid, return nil.
}
return h.hbuf.kv.musttoken(s)
return h.hbuf.musttoken(s)
}
// AppendRequest appends the request header representation to the buffer and returns the result.
func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
proto := h.Protocol()
if h.hbuf.kv.flags.HasAny(flagOOMReached) {
return dst, ErrBufferExhausted
} else if h.requestTarget.len == 0 || h.method.len == 0 {
if h.flags.hasAny(flagOOMReached) {
return dst, errOOM
} else if h.requestURI.len == 0 || h.method.len == 0 {
return dst, errNeedMethodURI
} else if len(proto) == 0 {
return dst, errNoProto
@@ -387,7 +398,7 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
} else {
dst = append(dst, method...)
}
uri := h.RequestTarget()
uri := h.RequestURI()
dst = append(dst, ' ')
dst = append(dst, uri...)
@@ -402,19 +413,9 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
// AppendResponse appends the response header representation to the buffer and returns the result.
func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
dst, err := h.AppendResponseNoHeaders(dst)
if err != nil {
return dst, err
}
dst = h.AppendHeaders(dst)
return append(dst, strCRLF...), nil
}
// AppendResponseNoHeaders appends the first line of the response containing protocol and status code/text: i.e: "HTTP/1.1 200 OK\r\n"
func (h *Header) AppendResponseNoHeaders(dst []byte) ([]byte, error) {
proto := h.Protocol()
if h.hbuf.kv.flags.HasAny(flagOOMReached) {
return dst, ErrBufferExhausted
if h.flags.hasAny(flagOOMReached) {
return dst, errOOM
} else if h.statusCode.len == 0 || h.statusText.len == 0 {
return dst, errBadStatusCodeTxt
} else if len(proto) == 0 {
@@ -428,24 +429,26 @@ func (h *Header) AppendResponseNoHeaders(dst []byte) ([]byte, error) {
dst = append(dst, ' ')
dst = append(dst, text...)
dst = append(dst, strCRLF...)
return dst, nil
dst = h.AppendHeaders(dst)
return append(dst, strCRLF...), nil
}
// AppendHeaders appends headers to buffer. Use AppendRequest and AppendResponse over this.
// Does not append extra \r\n to end. Appends nothing if contains no headers.
func (h *Header) AppendHeaders(dst []byte) []byte {
for i, kv := range h.hbuf.kv.kvs {
if kv.isValidHeader() {
k, v := h.hbuf.kv.At(i)
dst = appendHeaderLine(dst, b2s(k), b2s(v))
for i, n := 0, len(h.hbuf.headers); i < n; i++ {
kv := &h.hbuf.headers[i]
if kv.isValid() {
key := h.hbuf.musttoken(kv.key)
value := h.hbuf.musttoken(kv.value)
dst = appendHeaderLine(dst, b2s(key), b2s(value))
}
}
return dst
}
// String returns the header's wire representation, as a request if it has a
// request line and as a response otherwise. Returns the error text if neither
// can be built. Allocates, so it is meant for debugging and logging only.
func (h *Header) String() string {
buf, err := h.AppendRequest(nil)
if err != nil {
@@ -537,99 +540,3 @@ func CopyNormalizedHeaderValue(dst []byte, value []byte) (n int, modified bool)
}
return write, modified
}
// CopyDecodedPercentURL decodes percent-escapes in value into dst and returns bytes written.
// n < len(value) implies percent-escapes were decoded; the converse does not hold since
// '+' substitution preserves length. If plusAsSpace is set '+' decodes to ' ',
// which is correct for query and form-encoded data but NOT for path segments.
// On malformed escape returns n bytes written before the fault and a non-nil error.
// dst and value may only alias if &dst[0] == &value[0].
func CopyDecodedPercentURL(dst, value []byte, plusAsSpace bool) (n int, err error) {
if len(dst) < len(value) {
panic("httpraw.CopyDecodedPercentURL: dst buffer shorter than value")
}
read := 0
for {
escape := bytes.IndexByte(value[read:], '%')
if escape < 0 {
n += copyPlusDecoded(dst[n:], value[read:], plusAsSpace)
return n, nil
}
escape += read
n += copyPlusDecoded(dst[n:], value[read:escape], plusAsSpace)
if escape+2 >= len(value) {
return n, errBadPercentEncode // Truncated escape at end of value.
}
hi, okhi := unhexdigit(value[escape+1])
lo, oklo := unhexdigit(value[escape+2])
if !okhi || !oklo {
return n, errBadPercentEncode
}
// Write index n is always <= escape since decoding shrinks 3 bytes to 1,
// so writing here never clobbers an unread byte when dst aliases value.
dst[n] = hi<<4 | lo
n++
read = escape + 3
}
}
// EqualDecodedPercentURL reports whether value, once decoded, equals want. It
// decodes as it compares so it needs no scratch buffer, and reports false on a
// malformed escape just as [CopyDecodedPercentURL] errors on one.
// plusAsSpace decodes '+' to ' ', correct for query and form-encoded data but
// NOT for path segments.
func EqualDecodedPercentURL(value []byte, want string, plusAsSpace bool) bool {
w := 0
for i := 0; i < len(value); {
var c byte
switch {
case value[i] == '%':
if i+2 >= len(value) {
return false // Truncated escape at end of value.
}
hi, okhi := unhexdigit(value[i+1])
lo, oklo := unhexdigit(value[i+2])
if !okhi || !oklo {
return false
}
c = hi<<4 | lo
i += 3
case plusAsSpace && value[i] == '+':
c = ' '
i++
default:
c = value[i]
i++
}
if w >= len(want) || want[w] != c {
return false
}
w++
}
return w == len(want)
}
// copyPlusDecoded copies src to dst replacing '+' with ' ' if plusAsSpace set.
func copyPlusDecoded(dst, src []byte, plusAsSpace bool) int {
n := copy(dst, src)
if plusAsSpace {
for i := range n {
if dst[i] == '+' {
dst[i] = ' '
}
}
}
return n
}
func unhexdigit(c byte) (byte, bool) {
switch {
case c >= '0' && c <= '9':
return c - '0', true
case c >= 'a' && c <= 'f':
return c - 'a' + 10, true
case c >= 'A' && c <= 'F':
return c - 'A' + 10, true
}
return 0, false
}
+23 -305
View File
@@ -2,7 +2,6 @@ package httpraw
import (
"bytes"
"errors"
"fmt"
"net/http"
"strconv"
@@ -11,12 +10,6 @@ import (
"time"
)
// defaultKVCap is the key/value table size tests hand to Reset. A Reset with 0
// preserves whatever capacity the value already had, which is what production
// code wants on reuse but leaves a fresh value unable to hold a single pair when
// growth is disabled, see [Form.Reset].
const defaultKVCap = 16
func TestHeaderParseRequest(t *testing.T) {
const (
wantMethod = "GET"
@@ -57,8 +50,8 @@ func TestHeaderParseRequest(t *testing.T) {
if string(hdr.Method()) != wantMethod {
t.Errorf("want method %s, got %q", wantMethod, hdr.Method())
}
if !bytes.Equal(hdr.RequestTarget(), []byte(wantURI)) {
t.Errorf("want request URI %q, got %q", wantURI, hdr.RequestTarget())
if !bytes.Equal(hdr.RequestURI(), []byte(wantURI)) {
t.Errorf("want request URI %q, got %q", wantURI, hdr.RequestURI())
}
contentLength, _ := strconv.Atoi(string(hdr.Get("Content-Length")))
if contentLength != len(wantMessage) {
@@ -66,7 +59,7 @@ func TestHeaderParseRequest(t *testing.T) {
}
var c Cookie
cookie := hdr.Get("Cookie")
c.Reset(cookie, defaultKVCap)
c.Reset(cookie)
err = c.Parse()
if err != nil {
t.Error(err)
@@ -158,125 +151,6 @@ func strSameSite(mode http.SameSite) string {
}
}
func TestHeaderRequestPath(t *testing.T) {
for _, test := range []struct {
uri string
want string
}{
{uri: "/", want: "/"},
{uri: "/search?q=go", want: "/search"},
{uri: "/search?", want: "/search"},
{uri: "/a/b/c?x=1&y=2", want: "/a/b/c"},
{uri: "/?q=go", want: "/"},
} {
var h Header
err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n"))
if err != nil {
t.Fatal(err)
}
if got := string(h.RequestPath()); got != test.want {
t.Errorf("uri %q: want path %q, got %q", test.uri, test.want, got)
}
}
}
func TestHeaderContentLength(t *testing.T) {
for _, test := range []struct {
field string // Extra header lines, empty for an absent field.
want int64
wantErr error
}{
{field: "Content-Length: 0", want: 0},
{field: "Content-Length: 12", want: 12},
{field: "Content-Length: 12 ", want: 12}, // OWS around the value, RFC 9110 5.6.3.
{field: "Content-Length: 9223372036854775807", want: 9223372036854775807},
{field: ""}, // Absent field is no body and no error, RFC 9112 6.3.
{field: "Content-Length:", wantErr: errBadContentLength}, // Present but empty.
{field: "Content-Length: -1", wantErr: errBadContentLength}, // Digits only, RFC 9112 6.2.
{field: "Content-Length: 1 2", wantErr: errBadContentLength}, // Not a list.
{field: "Content-Length: 9223372036854775808", wantErr: errBadContentLength},
} {
var h Header
raw := "POST / HTTP/1.1\r\nHost: h\r\n"
if test.field != "" {
raw += test.field + "\r\n"
}
if err := h.ParseBytes(false, []byte(raw+"\r\n")); err != nil {
t.Fatal(err)
}
got, present, err := h.ContentLength()
if err != test.wantErr {
t.Errorf("%q: want error %v, got %v", test.field, test.wantErr, err)
} else if err == nil && got != test.want {
t.Errorf("%q: want %d, got %d", test.field, test.want, got)
}
key, _, _ := strings.Cut(test.field, ":")
if strings.EqualFold(key, headerContentLength) != present {
t.Error("unexpected 'present'", test.field)
}
}
}
func TestNextQueryPair(t *testing.T) {
for _, test := range []struct {
uri string
want string // "key=value" pairs joined by '|'; nil value shown as "key".
}{
{uri: "/", want: ""},
{uri: "/x?", want: ""},
{uri: "/x?q=go", want: "q=go"},
{uri: "/x?q=go&n=1", want: "q=go|n=1"},
{uri: "/x?debug&q=go", want: "debug|q=go"}, // No '=' yields a nil value.
{uri: "/x?q=", want: "q="}, // Empty but present value.
{uri: "/x?&&q=go&", want: "q=go"}, // Empty sequences skipped.
{uri: "/x?=v", want: "=v"}, // Empty name is kept.
{uri: "/x?a=1&a=2", want: "a=1|a=2"}, // Duplicates all yielded.
{uri: "/x?a%20b=c%20d", want: "a%20b=c%20d"}, // Raw, undecoded.
{uri: "/x?a=b=c", want: "a=b=c"}, // Only first '=' splits.
} {
var h Header
err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n"))
if err != nil {
t.Fatal(err)
}
var got []byte
rawkey, rawval, rest := NextQueryPair(h.RequestQuery())
for rawkey != nil {
if len(got) > 0 {
got = append(got, '|')
}
got = append(got, rawkey...)
if rawval != nil {
got = append(got, '=')
got = append(got, rawval...)
}
rawkey, rawval, rest = NextQueryPair(rest)
}
if string(got) != test.want {
t.Errorf("uri %q: want %q, got %q", test.uri, test.want, got)
}
}
}
// A nil key ends iteration, and stopping early is just not looping again.
func TestNextQueryPairEnd(t *testing.T) {
rawkey, rawval, rest := NextQueryPair([]byte("a=1&b=2"))
if string(rawkey) != "a" || string(rawval) != "1" || string(rest) != "b=2" {
t.Fatalf("want a=1 with rest b=2, got %q=%q rest %q", rawkey, rawval, rest)
}
rawkey, _, rest = NextQueryPair(rest)
if string(rawkey) != "b" || len(rest) != 0 {
t.Fatalf("want b with empty rest, got %q rest %q", rawkey, rest)
}
if rawkey, _, _ = NextQueryPair(rest); rawkey != nil {
t.Fatalf("want nil key at end of query, got %q", rawkey)
}
// Trailing separators yield no pair rather than an empty one.
if rawkey, _, _ = NextQueryPair([]byte("&&")); rawkey != nil {
t.Fatalf("want nil key for empty sequences, got %q", rawkey)
}
}
func TestHeaderNormalizeKey(t *testing.T) {
var tests = []struct {
key string
@@ -335,72 +209,11 @@ func TestCopyNormalizedHeaderValue(t *testing.T) {
}
}
func TestCopyDecodedPercentURL(t *testing.T) {
var tests = []struct {
value string
plusAsSpace bool
want string
wantErr bool
}{
{value: "", want: ""},
{value: "/plain/path", want: "/plain/path"},
{value: "/a%20b", want: "/a b"},
{value: "%41%42%43", want: "ABC"},
{value: "%2f%2F", want: "//"}, // lower and upper case hex digits.
{value: "%25", want: "%"}, // escaped percent must not re-trigger decoding.
{value: "%2525", want: "%25"}, // decoded output is not re-scanned.
{value: "a+b", want: "a+b"}, // plus is literal in path segments.
{value: "a+b", plusAsSpace: true, want: "a b"},
{value: "%20+%20", plusAsSpace: true, want: " "},
{value: "/x?q=%E2%82%AC", want: "/x?q=\xe2\x82\xac"}, // multi-byte UTF-8 sequence.
// Malformed escapes must error, never pass through silently.
{value: "%zz", want: "", wantErr: true},
{value: "ok%4", want: "ok", wantErr: true}, // truncated escape at end.
{value: "ok%", want: "ok", wantErr: true}, // bare percent at end.
{value: "a%2gb", want: "a", wantErr: true}, // second digit not hex.
{value: "a%g2b", want: "a", wantErr: true}, // first digit not hex.
}
dst := make([]byte, 256)
for _, test := range tests {
value := []byte(test.value)
n, err := CopyDecodedPercentURL(dst[:len(value)], value, test.plusAsSpace)
if test.wantErr && err == nil {
t.Errorf("%q: want error, got nil", test.value)
} else if !test.wantErr && err != nil {
t.Errorf("%q: unexpected error %s", test.value, err)
}
if got := string(dst[:n]); got != test.want {
t.Errorf("%q: want %q got %q", test.value, test.want, got)
}
// n<len(value) implies percent-escapes were decoded. The converse does not
// hold: '+'->' ' substitution preserves length.
if !test.wantErr && n < len(test.value) && test.want == test.value {
t.Errorf("%q: n=%d signals decoding but value unchanged", test.value, n)
}
if !test.wantErr && !test.plusAsSpace && test.want != test.value && n >= len(test.value) {
t.Errorf("%q: n=%d does not signal decoding of %q", test.value, n, test.want)
}
}
}
// In-place decoding (dst aliasing value at offset 0) must yield the same result.
func TestCopyDecodedPercentURLInPlace(t *testing.T) {
const value, want = "/a%20b%2Fc%25", "/a b/c%"
buf := []byte(value)
n, err := CopyDecodedPercentURL(buf, buf, false)
if err != nil {
t.Fatal(err)
}
if got := string(buf[:n]); got != want {
t.Fatalf("want %q got %q", want, got)
}
}
func TestHeaderSetOverwrite(t *testing.T) {
var h Header
h.Reset(nil, defaultKVCap)
h.Reset(nil)
h.SetMethod("GET")
h.SetRequestTarget("/")
h.SetRequestURI("/")
h.SetProtocol("HTTP/1.1")
h.Set("Host", "first.example.com")
@@ -421,7 +234,7 @@ func TestHeaderSetOverwrite(t *testing.T) {
func TestHeaderSetBytesEmptyValue(t *testing.T) {
var h Header
h.Reset(nil, defaultKVCap)
h.Reset(nil)
h.SetBytes("X-Empty", nil)
if got := h.Get("X-Empty"); len(got) != 0 {
t.Errorf("want empty value, got %q", got)
@@ -455,7 +268,7 @@ func TestHeader_LargeBufferOverflow(t *testing.T) {
}
// a complete but malformed header line with no colon must be a hard error,
// not ErrNeedMoreData (which makes a streaming parser wait forever).
// not errNeedMore (which makes a streaming parser wait forever).
func TestHeader_ColonlessLineIsHardError(t *testing.T) {
raw := "GET / HTTP/1.1\r\nBadHeaderNoColon\r\n\r\n"
var h Header
@@ -463,8 +276,8 @@ func TestHeader_ColonlessLineIsHardError(t *testing.T) {
if err == nil {
t.Fatal("want error on colonless header line, got nil")
}
if err == ErrNeedMoreData {
t.Fatalf("colonless line reported as ErrNeedMoreData (parser would hang); want a hard error like errInvalidName")
if err == errNeedMore {
t.Fatalf("colonless line reported as errNeedMore (parser would hang); want a hard error like errInvalidName")
}
}
@@ -476,19 +289,19 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) {
const part2 = ": example.com\r\n\r\n"
var h Header
h.Reset(nil, defaultKVCap)
if err := h.ReadFromBytes([]byte(part1)); err != nil {
h.Reset(nil)
if _, err := h.ReadFromBytes([]byte(part1)); err != nil {
t.Fatal(err)
}
needMore, err := h.TryParse(false)
if err != nil && err != ErrNeedMoreData {
t.Fatalf("split before colon: want ErrNeedMoreData/nil, got %v", err)
if err != nil && err != errNeedMore {
t.Fatalf("split before colon: want errNeedMore/nil, got %v", err)
}
if !needMore {
t.Fatal("want needMoreData=true after partial input")
}
if err := h.ReadFromBytes([]byte(part2)); err != nil {
if _, err := h.ReadFromBytes([]byte(part2)); err != nil {
t.Fatal(err)
}
needMore, err = h.TryParse(false)
@@ -509,7 +322,7 @@ func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
const key, value = "K", "V"
buf := make([]byte, 0, len(key)+len(value)) // exact cap, no slack.
var h Header
h.Reset(buf, defaultKVCap)
h.Reset(buf)
defer func() {
if r := recover(); r != nil {
t.Fatalf("appendHeader panicked on exact-cap buffer: %v", r)
@@ -526,10 +339,10 @@ func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) {
func TestHeader_AddFullBufferNoPanic(t *testing.T) {
buf := make([]byte, 0, 40) // Small cap; enough for Reset (len 0) but not the field below.
var h Header
h.Reset(buf, defaultKVCap)
h.ConfigBufferGrowth(false)
h.Reset(buf)
h.EnableBufferGrowth(false)
h.SetMethod("GET")
h.SetRequestTarget("/")
h.SetRequestURI("/")
h.SetProtocol("HTTP/1.1")
defer func() {
@@ -561,7 +374,7 @@ func TestHeader_SetInt(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
var h Header
h.Reset(nil, defaultKVCap)
h.Reset(nil)
h.SetInt("Content-Length", tc.value, tc.base)
if got := string(h.Get("Content-Length")); got != tc.want {
t.Fatalf("want %q, got %q", tc.want, got)
@@ -573,9 +386,9 @@ func TestHeader_SetInt(t *testing.T) {
// SetInt on an existing key must reuse the slot in place (single field, latest value).
func TestHeader_SetIntOverwrite(t *testing.T) {
var h Header
h.Reset(nil, defaultKVCap)
h.Reset(nil)
h.SetMethod("GET")
h.SetRequestTarget("/")
h.SetRequestURI("/")
h.SetProtocol("HTTP/1.1")
h.SetInt("Content-Length", 100, 10)
@@ -597,8 +410,8 @@ func TestHeader_SetIntOverwrite(t *testing.T) {
func TestHeader_SetIntNoAlloc(t *testing.T) {
buf := make([]byte, 0, 256)
var h Header
h.Reset(buf, defaultKVCap)
h.ConfigBufferGrowth(false)
h.Reset(buf)
h.EnableBufferGrowth(false)
h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot.
allocs := testing.AllocsPerRun(100, func() {
h.SetInt("Content-Length", 1234567890, 10)
@@ -610,98 +423,3 @@ func TestHeader_SetIntNoAlloc(t *testing.T) {
t.Fatalf("want %q, got %q", "1234567890", got)
}
}
// A browser sends upwards of twenty header fields and an API client with a few
// custom fields is not far behind. The field table must be sized from the
// buffer the caller handed over, not fixed at a count that a real request
// exceeds.
func TestHeader_FieldTableSizedFromBuffer(t *testing.T) {
const wantVal = "the-canary-value"
var raw strings.Builder
raw.WriteString("GET / HTTP/1.1\r\nHost: lneto.test\r\n")
for i := range 40 {
raw.WriteString("X-Field-")
raw.WriteString(strconv.Itoa(i))
raw.WriteString(": value-of-a-realistic-length-here\r\n")
}
raw.WriteString("X-Canary: " + wantVal + "\r\n\r\n")
var h Header
h.Reset(make([]byte, 0, 8192), defaultKVCap) // Room for the block with plenty to spare.
err := h.ParseBytes(false, []byte(raw.String()))
if err != nil {
t.Fatalf("parsing a 42 field request into an 8kB buffer: %s", err)
}
if got := string(h.Get("X-Canary")); got != wantVal {
t.Fatalf("want X-Canary %q, got %q", wantVal, got)
}
}
// A buffer too small for the fields it is handed must be refused with an error
// the caller can act on, so a server answers 431 instead of dropping the peer.
func TestHeader_FieldTableFullIsReported(t *testing.T) {
var raw strings.Builder
raw.WriteString("GET / HTTP/1.1\r\n")
for i := range 64 {
// As short as a field gets.
raw.WriteString("H")
raw.WriteString(strconv.Itoa(i))
raw.WriteString(":v\r\n")
}
raw.WriteString("\r\n")
var h Header
h.Reset(make([]byte, 0, 512), defaultKVCap)
h.ConfigBufferGrowth(false)
err := h.ParseBytes(false, []byte(raw.String()))
if !errors.Is(err, ErrHeaderTooMany) {
t.Fatalf("want ErrHeaderFieldsTooLarge, got %v", err)
}
}
// EqualDecodedPercentURL must agree with CopyDecodedPercentURL on every input:
// same decoded bytes, and false wherever the copying decoder reports an error.
func TestEqualDecodedPercentURL(t *testing.T) {
for _, value := range []string{
"", "plain", "a+b", "a%20b", "%41%42", "100%25", "a%2Fb", "+", "%2b",
"trailing%", "trailing%4", "%zz", "a%2", "%%", "a+b%20c", "%00",
} {
for _, plusAsSpace := range []bool{false, true} {
dst := make([]byte, len(value))
n, err := CopyDecodedPercentURL(dst, []byte(value), plusAsSpace)
// The copying decoder is the reference: whatever it produces is what
// an equal comparison must accept, and only that.
want := ""
if err == nil {
want = string(dst[:n])
}
got := EqualDecodedPercentURL([]byte(value), want, plusAsSpace)
if err != nil {
if got {
t.Errorf("%q plus=%v: malformed escape must not compare equal", value, plusAsSpace)
}
continue
}
if !got {
t.Errorf("%q plus=%v: want equal to its own decoding %q", value, plusAsSpace, want)
}
if EqualDecodedPercentURL([]byte(value), want+"x", plusAsSpace) {
t.Errorf("%q plus=%v: must not equal a longer want", value, plusAsSpace)
}
if want != "" && EqualDecodedPercentURL([]byte(value), want[:len(want)-1], plusAsSpace) {
t.Errorf("%q plus=%v: must not equal a shorter want", value, plusAsSpace)
}
}
}
}
// The comparison must not allocate: it is the reason a query lookup can return
// a view without scratch space.
func TestEqualDecodedPercentURLNoAlloc(t *testing.T) {
value := []byte("a%20long%2Dish+key")
allocs := testing.AllocsPerRun(100, func() {
EqualDecodedPercentURL(value, "a long-ish key", true)
})
if allocs != 0 {
t.Fatalf("EqualDecodedPercentURL allocated %v times, want 0", allocs)
}
}
-504
View File
@@ -1,504 +0,0 @@
package httpraw
import (
"io"
"slices"
"strconv"
"unsafe"
"github.com/soypat/lneto/internal"
)
// kvBuffer is a common key-value store engine for Cookie, Form, Header and other HTTP abstractions that need
// a key-value store with underlying buffer memory.
type kvBuffer struct {
buf []byte
kvs []pairKV
flags Flags
}
func (kvb *kvBuffer) free() int { return cap(kvb.buf) - len(kvb.buf) }
// BufferRaw returns the underlying buffer, its length being the portion in use.
// Stored pairs alias it, so writing to it mangles them.
func (kvb *kvBuffer) BufferRaw() []byte { return kvb.buf }
// BufferUsed returns the raw memory used, which is what a caller appending from
// several sources checks to know whether a separator is needed. Counts buffered
// bytes and not parsed pairs, so it is set before a Parse and unchanged by one.
func (kvb *kvBuffer) BufferUsed() int { return len(kvb.buf) }
// EnableBufferGrowth allows the buffer to grow past the memory [kvBuffer.Reset]
// was handed. The setting outlives Reset; with growth off callers get [ErrBufferExhausted].
func (kvb *kvBuffer) EnableBufferGrowth(enableGrowth bool) {
if enableGrowth {
kvb.flags &^= flagNoBufferGrow
} else {
kvb.flags |= flagNoBufferGrow
}
}
func (kvb *kvBuffer) discardKVs() { kvb.kvs = kvb.kvs[:0] }
// BufferGrowthEnabled reports whether the buffer may grow, see [kvBuffer.EnableBufferGrowth].
func (kvb *kvBuffer) BufferGrowthEnabled() bool { return !kvb.flags.HasAny(flagNoBufferGrow) }
// ReadFromBytes appends buf to the underlying buffer, accumulating data to parse.
// Returns [ErrBufferExhausted] when buf does not fit and growth is disabled.
func (kvb *kvBuffer) ReadFromBytes(buf []byte) error {
if len(buf) == 0 {
return io.ErrNoProgress // Nothing handed over, not a buffer problem.
} else if kvb.flags.HasAny(flagMangledBuffer) {
return errMangledBuffer
} else if len(buf)+cap(kvb.buf) > maxBufLen {
return ErrBufferExhausted
}
free := kvb.free()
if len(buf) > free && !kvb.BufferGrowthEnabled() {
return ErrBufferExhausted
}
kvb.buf = append(kvb.buf, buf...)
return nil
}
// ReadLimited appends at most limit bytes read from r to the underlying buffer.
// A read returning data alongside [io.EOF] reports a nil error, later ones io.EOF.
func (kvb *kvBuffer) ReadLimited(r io.Reader, limit int) (int, error) {
free := kvb.free()
growthEnabled := kvb.BufferGrowthEnabled()
if !growthEnabled && (free == 0 || free < limit) || len(kvb.buf) >= maxBufLen {
return 0, ErrBufferExhausted
} else if kvb.flags.HasAny(flagMangledBuffer) {
return 0, errMangledBuffer
} else if kvb.flags.HasAny(flagReaderEOF) {
return 0, io.EOF
} else if limit <= 0 {
return 0, io.ErrNoProgress
}
kvb.buf = slices.Grow(kvb.buf, limit)
n, err := r.Read(kvb.buf[len(kvb.buf):min(len(kvb.buf)+limit, maxBufLen)])
kvb.buf = kvb.buf[:len(kvb.buf)+n]
if err != nil {
if n > 0 && err == io.EOF {
kvb.flags |= flagReaderEOF
err = nil // Nil out EOF to not scare off readers.
}
}
return n, err
}
// Reset discards all pairs and takes buf as the buffer to parse in place, nil
// reusing the current one. kvCap sizes the pair table. Only the growth setting survives.
func (kvb *kvBuffer) Reset(buf []byte, kvCap int) {
if buf == nil {
kvb.buf = kvb.buf[:0]
} else {
kvb.buf = buf
}
internal.SliceReuse(&kvb.kvs, kvCap)
kvb.flags = kvb.flags & flagNoBufferGrow // Only flag persisted is buffer grow config.
}
// CopyFrom replaces the receiver's contents with a copy of src, sharing no
// memory with it afterwards.
func (kvb *kvBuffer) CopyFrom(src *kvBuffer) {
kvb.buf = append(kvb.buf[:0], src.buf...)
kvb.kvs = append(kvb.kvs[:0], src.kvs...)
}
// Get returns the value of the first pair matching key.
// Bytes are compared as stored, so if using a Form call [Form.Decode] first when keys may be encoded.
// Returns nil for an absent key and for a valueless pair alike, so use
// [kvBuffer.Present] to tell the two apart.
func (kvb *kvBuffer) Get(key string) []byte {
i := kvb.getIdx(key)
if i < 0 {
return nil
}
return kvb.AtValue(i)
}
// GetFold returns the value of the first key that matches ascii-case-insensitive.
func (kvb *kvBuffer) GetFold(key string) []byte {
i := kvb.getFoldIdx(key)
if i < 0 {
return nil
}
return kvb.AtValue(i)
}
// ForEach iterates over the cookie's key-value pairs as stored until cb returns false.
func (kvb *kvBuffer) ForEach(cb func(key, value []byte) bool) {
nc := len(kvb.kvs)
for i := range nc {
if !kvb.kvs[i].isValid() {
continue
} else if !cb(kvb.At(i)) {
break
}
}
}
// Has returns true if key is present, with or without a value.
func (kvb *kvBuffer) Present(key string) bool { // TODO: rename to Has.
return kvb.getIdx(key) >= 0
}
// Has returns true if key is present, with or without a value.
func (kvb *kvBuffer) HasKeyValue(key, value string) bool {
idx := kvb.getIdx(key)
if idx >= 0 {
return b2s(kvb.musttoken(kvb.kvs[idx].value)) == value
}
return false
}
// Add appends a pair, keeping any already sharing the key: use [kvBuffer.Set]
// to replace instead. Reports false if the buffer could not hold it.
func (kvb *kvBuffer) Add(key, value string) (enoughSpace bool) {
kvb.appendPair(key, value)
return kvb.getIdx(key) >= 0
}
// Set replaces key's value and invalidates every other pair sharing the key, so
// a following [kvBuffer.Get] sees exactly one value.
//
// It rewrites in place when it can: of the pairs it would invalidate it keeps
// the smallest whose key and value regions both still hold the new pair,
// leaving the roomier regions for a later Set. When none fits the pair is
// appended with [kvBuffer.Add] and the invalidated regions are stranded, since
// nothing here compacts the buffer.
func (kvb *kvBuffer) Set(key, value string) (enoughSpace bool) {
reuse := kvb.takeReusableSlot(key, len(key), len(value))
if reuse < 0 {
return kvb.Add(key, value)
}
kvb.overwriteAt(reuse, key, value)
return true
}
// SetInt is [kvBuffer.Set]'s integer counterpart. It formats value straight into
// the slot it reuses, so overwriting a pair never allocates.
func (kvb *kvBuffer) SetInt(key string, value int64, base int) (enoughSpace bool) {
reuse := kvb.takeReusableSlot(key, len(key), internal.IntLen(value, base))
if reuse < 0 {
return kvb.appendPairInt(key, value, base)
}
kvb.flags |= flagMangledBuffer
kv := &kvb.kvs[reuse]
copy(kvb.buf[kv.key.start:], key)
kv.key.len = tokint(len(key))
// The slot was picked to hold keyLen/valueLen, so AppendInt writes inside
// buf and never grows a new backing array.
v := strconv.AppendInt(kvb.buf[kv.value.start:kv.value.start], value, base)
kv.value.len = tokint(len(v))
return true
}
// takeReusableSlot invalidates every pair matching key except the smallest one
// whose key and value regions hold keyLen and valueLen bytes, whose index it
// returns. It returns -1 when no surviving slot fits, meaning the caller must
// append instead.
func (kvb *kvBuffer) takeReusableSlot(key string, keyLen, valueLen int) int {
reuse := -1
for i := range kvb.kvs {
kv := &kvb.kvs[i]
if !kv.isValid() || b2s(kvb.musttoken(kv.key)) != key {
continue
}
// A valueless pair holds no value region, so reusing one would write the
// value over byte 0. Let it fall through to the caller's append, which
// gives the pair a real region and keeps "ok" distinct from "ok=".
fits := kv.HasValue() && int(kv.key.len) >= keyLen && int(kv.value.len) >= valueLen
if fits && (reuse < 0 || kv.size() < kvb.kvs[reuse].size()) {
if reuse >= 0 {
kvb.kvs[reuse].invalidate() // Superseded by a tighter fit.
}
reuse = i
continue
}
kv.invalidate()
}
return reuse
}
// overwriteAt writes key and value over the regions pair i already owns. The
// caller must have checked both fit; the bytes freed by a shorter pair are
// stranded, not reclaimed.
func (kvb *kvBuffer) overwriteAt(i int, key, value string) {
kvb.flags |= flagMangledBuffer
kv := &kvb.kvs[i]
copy(kvb.buf[kv.key.start:], key)
kv.key.len = tokint(len(key))
copy(kvb.buf[kv.value.start:], value)
kv.value.len = tokint(len(value))
}
func (kvb *kvBuffer) setInternal(key, value []byte) (enoughSpace bool) {
if !kvb.canAddOneKV() {
return false
}
kvb.flags |= flagKVAppended
kvb.kvs = append(kvb.kvs, pairKV{
key: kvb.view(key),
value: kvb.view(value),
})
return true
}
// Len returns the number of slots stored, counting those [kvBuffer.Set] invalidated.
func (kvb *kvBuffer) Len() int { return len(kvb.kvs) }
// At returns the i'th pair in wire order. value is nil for a pair holding none,
// which is what tells a form's "ok" from "ok=".
func (kvb *kvBuffer) At(i int) (key, value []byte) {
kv := kvb.kvs[i]
if !kv.HasValue() {
return kvb.musttoken(kv.key), nil
}
return kvb.musttoken(kv.key), kvb.musttoken(kv.value)
}
func (kvb *kvBuffer) setAt(i int, k, v []byte) {
kvb.flags |= flagMangledBuffer
// Route through slice, not bytes2tok: a nil v is a pair with no '=' and must
// stay absent rather than trip the alias check on a nil pointer.
kvb.kvs[i] = pairKV{
key: kvb.view(k),
value: kvb.view(v),
}
}
// AtKey is [kvBuffer.At] limited to the i'th key.
func (kvb *kvBuffer) AtKey(i int) (key []byte) { return kvb.musttoken(kvb.kvs[i].key) }
// AtValue is [kvBuffer.At] limited to the i'th value, nil when the pair holds none.
func (kvb *kvBuffer) AtValue(i int) (key []byte) {
if !kvb.kvs[i].HasValue() {
return nil
}
return kvb.musttoken(kvb.kvs[i].value)
}
func (kvb *kvBuffer) getIdx(key string) int {
for i, pair := range kvb.kvs {
if pair.isValid() && b2s(kvb.musttoken(pair.key)) == key {
return i
}
}
return -1
}
func (kvb *kvBuffer) getFoldIdx(key string) int {
for i, pair := range kvb.kvs {
if pair.isValid() && EqualFoldASCII(key, b2s(kvb.AtKey(i))) {
return i
}
}
return -1
}
// EqualFoldASCII reports whether a and b are equal under ASCII case folding.
// Unlike strings.EqualFold it does not fold non-ASCII runes, so no multi-byte
// rune such as U+212A KELVIN SIGN can alias a header key.
func EqualFoldASCII(a, b string) bool {
if len(a) != len(b) {
return false
}
const asciiCapDiff = 'a' - 'A'
for i := 0; i < len(a); i++ {
ca, cb := a[i], b[i]
if ca >= 'A' && ca <= 'Z' {
ca += asciiCapDiff
}
if cb >= 'A' && cb <= 'Z' {
cb += asciiCapDiff
}
if ca != cb {
return false
}
}
return true
}
// reserve ensures need free bytes are available in the buffer, growing it when
// permitted. It accounts for the byte-0 reservation on an empty buffer (see
// mustAppendSlice). It returns false and sets flagOOMReached when the space
// cannot be guaranteed: a tokint offset overflow, or a full buffer with
// flagNoBufferGrow set.
func (kvb *kvBuffer) reserve(need int) (enoughSpace bool) {
if len(kvb.buf) == 0 {
need++ // mustAppend* reserves byte 0 on an empty buffer.
}
if len(kvb.buf)+need > maxBufLen {
kvb.flags |= flagOOMReached // Offsets would overflow uint16 tokint.
return false
}
if need > kvb.free() {
if kvb.flags.HasAny(flagNoBufferGrow) {
kvb.flags |= flagOOMReached
return false
}
kvb.buf = slices.Grow(kvb.buf, need)
}
return true
}
func (kvb *kvBuffer) appendPair(key, value string) bool {
if !kvb.canAddOneKV() || !kvb.reserve(len(key)+len(value)) {
return false
}
kvb.flags |= flagKVAppended
kvb.kvs = append(kvb.kvs, pairKV{
key: kvb.mustAppendSlice(key),
value: kvb.mustAppendSlice(value),
})
return true
}
func (kvb *kvBuffer) appendPairInt(key string, value int64, base int) bool {
vlen := internal.IntLen(value, base)
if !kvb.canAddOneKV() || !kvb.reserve(len(key)+vlen) {
return false
}
kvb.flags |= flagKVAppended
kvb.kvs = append(kvb.kvs, pairKV{
key: kvb.mustAppendSlice(key),
value: kvb.mustAppendInt(value, base),
})
return true
}
func (kvb *kvBuffer) canAddOneKV() (enoughSpace bool) {
return len(kvb.kvs) < cap(kvb.kvs) || kvb.flags&flagNoBufferGrow == 0
}
func (kvb *kvBuffer) mustAppendSlice(value string) view {
L := len(kvb.buf)
if L == 0 {
L++ // Valid key-values start after 0.
}
copy(kvb.buf[L:L+len(value)], value)
kvb.buf = kvb.buf[:L+len(value)]
return kvb.view(kvb.buf[L : L+len(value)])
}
func (kvb *kvBuffer) mustAppendInt(value int64, base int) view {
L := len(kvb.buf)
if L == 0 {
L++ // Valid key-values start after byte 0.
}
v := strconv.AppendInt(kvb.buf[L:L], value, base)
kvb.buf = kvb.buf[:L+len(v)]
return kvb.view(kvb.buf[L : L+len(v)])
}
// reuseOrAppend writes value over tok's slot when it fits there, avoiding any
// buffer growth; otherwise it appends a fresh slot.
func (kvb *kvBuffer) reuseOrAppend(tok view, value string) view {
if tok.len > tokint(len(value)) {
copy(kvb.musttoken(tok), value)
tok.len = tokint(len(value))
return tok
}
return kvb.appendSlice(value)
}
// appendSlice reserves space (growing or flagging OOM) and appends value as a
// new slot.
func (kvb *kvBuffer) appendSlice(value string) view {
debuglog("http:appendslice:start")
if !kvb.reserve(len(value)) {
return view{} // Drop and flag OOM; never panic.
}
kvb.flags |= flagMangledBuffer
return kvb.mustAppendSlice(value)
}
// reuseOrAppendInt is [kvBuffer.reuseOrAppend]'s integer counterpart.
func (kvb *kvBuffer) reuseOrAppendInt(tok view, value int64, base int) view {
n := internal.IntLen(value, base)
if int(tok.len) >= n {
// Reuse: format directly over the existing slot. No free space needed
// since n <= tok.len and the slot already lives inside buf.
v := strconv.AppendInt(kvb.buf[tok.start:tok.start], value, base)
tok.len = tokint(len(v))
kvb.flags |= flagMangledBuffer
return tok
}
return kvb.appendInt(value, base, n)
}
// appendInt reserves space (growing or flagging OOM) and appends value as a new slot.
func (kvb *kvBuffer) appendInt(value int64, base, n int) view {
if !kvb.reserve(n) {
return view{} // Drop and flag OOM; never panic.
}
kvb.flags |= flagMangledBuffer
return kvb.mustAppendInt(value, base)
}
func (kvb *kvBuffer) view(value []byte) view {
if value == nil {
return view{}
}
return bytes2tok(kvb.buf, value)
}
func (kvb kvBuffer) musttoken(slice view) []byte {
return tok2bytes(kvb.buf, slice)
}
func (kvb *kvBuffer) noKV() pairKV { return pairKV{} }
type tokint = uint16
// view is a smaller `string`-like representation of a section in [kvBuffer]'s buffer.
type view struct {
start tokint
len tokint
}
type pairKV struct {
key view
value view // value start >0 means value is present.
}
// isValid is for stores parsed in place, where offset 0 is the first key so
// only length can signal presence. Empty keys are valid: see valueless cookies.
func (pair pairKV) isValid() bool {
return pair.key.len > 0 || pair.value.len > 0
}
// isValidHeader is for the append-built [Header] store, where mustAppendSlice
// burns byte 0 so a zero offset means absent. Drops offset-0 pairs otherwise.
func (pair pairKV) isValidHeader() bool { return pair.key.start > 0 }
func (pair *pairKV) invalidate() {
*pair = pairKV{}
}
// size is the buffer a pair occupies, used to pick the tightest slot to reuse.
func (pair pairKV) size() int { return int(pair.key.len) + int(pair.value.len) }
func (pair pairKV) HasValue() bool { return pair.value.start > 0 }
// b2s converts byte slice to a string without memory allocation.
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
func b2s(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
func tok2bytes(buf []byte, slice view) []byte {
return buf[slice.start : slice.start+slice.len]
}
func bytes2tok(buf, value []byte) view {
base := uintptr(unsafe.Pointer(unsafe.SliceData(buf)))
off := uintptr(unsafe.Pointer(unsafe.SliceData(value)))
if off < base || off > base+uintptr(len(buf)) {
panic("httpx: argument buffer does not alias header buffer")
}
return view{
start: tokint(off - base),
len: tokint(len(value)),
}
}
-272
View File
@@ -1,272 +0,0 @@
package httpraw
import (
"bytes"
"io"
)
// Multipart splits a "multipart/form-data" body into its parts. Such bodies
// frame their fields with a delimiter instead of escaping them, so a part's
// value has no length: it ends where the next delimiter begins.
//
// Multipart stores none of the body, leaving the caller to decide what to keep,
// what to skip and when a part has grown too large. Both methods report how much
// of buf they consumed, which the caller compacts away before reading more in:
//
// var m httpraw.Multipart
// m.SetContentType(contentType)
// var hdr httpraw.MultipartHeader
// for {
// parsed, err := m.NextHeader(&hdr, buf[:buflen])
// if err != nil {
// break // io.EOF at the closing delimiter, body done.
// } else if parsed == 0 {
// // Header block incomplete: read more into buf[buflen:] and retry.
// continue
// }
// buflen = copy(buf, buf[parsed:buflen])
// for {
// bodyLen, restOff, done := m.NextBody(buf[:buflen])
// // Consume buf[:bodyLen] for hdr.Name, then compact and read more.
// buflen = copy(buf, buf[restOff:buflen])
// if done {
// break
// }
// }
// }
type Multipart struct {
// Boundary is the delimiter parameter of the body's Content-Type field,
// without the leading "--" the delimiter carries on the wire.
Boundary []byte
}
// MultipartHeader is a part's header block and the Content-Disposition
// parameters that identify it.
type MultipartHeader struct {
// PartView is the part's raw header block, ending in its final CRLF. It
// aliases the buffer it was parsed from, so it is only valid until that
// buffer is compacted or read into again.
PartView []byte
// Name is the name parameter of a part's Content-Disposition field,
// i.e: "photo" for `form-data; name="photo"; filename="beach.png"`.
// Copied out of the buffer, so it outlives it, and reused between parts.
Name []byte
// Filename is the filename parameter of a part's Content-Disposition
// field, empty when the part is not a file upload. Copied like Name.
Filename []byte
}
// Reset clears the header for the next part, keeping the buffers Name and
// Filename were copied into so a reused header stops allocating.
func (hdr *MultipartHeader) Reset() {
hdr.PartView = nil
hdr.Name = hdr.Name[:0]
hdr.Filename = hdr.Filename[:0]
}
// SetContentType sets [Multipart.Boundary] from the boundary parameter of a
// Content-Type field value, i.e: "abc123" for
// "multipart/form-data; boundary=abc123". The leading "--" the delimiter carries
// on the wire is not included. Fails when the parameter is absent or is not
// 1 to 70 characters long, RFC 2046 5.1.1; a zero length boundary would match
// every "--" in the body.
func (m *Multipart) SetContentType(contentType []byte) error {
m.Boundary = ContentParam(contentType, "boundary")
if len(m.Boundary) == 0 || len(m.Boundary) > 70 {
return errNoBoundary // RFC 2046 5.1.1: 1..70 characters, required.
}
return nil
}
// NextHeader parses the leading part's header block off a multipart body into
// dst, returning how many bytes of data it consumed: the part's content begins
// at data[parsedLen]. A zero parsedLen and no error means data holds no complete
// delimiter and header block yet, so the caller reads more in and retries.
// Returns [io.EOF] once the closing delimiter is reached. dst is reset on error.
func (m *Multipart) NextHeader(dst *MultipartHeader, data []byte) (parsedLen int, err error) {
dst.Reset()
if len(m.Boundary) == 0 {
return 0, errNoBoundary
}
idx := m.indexDelimiter(data)
if idx < 0 {
return 0, nil
}
after := idx + len("--") + len(m.Boundary)
if after+2 > len(data) {
return 0, nil // Cannot tell a closing delimiter yet.
} else if data[after] == '-' && data[after+1] == '-' {
return 0, io.EOF
}
// Delimiter is followed by CRLF, then the part's header block.
if data[after] == '\r' {
after++
}
if after >= len(data) {
return 0, nil
} else if data[after] != '\n' {
return 0, errBadDelimiter
}
after++
end := bytes.Index(data[after:], []byte("\r\n\r\n"))
if end < 0 {
return 0, nil
}
dst.PartView = data[after : after+end+2]
disposition := partField(dst.PartView)
dst.Name = append(dst.Name[:0], ContentParam(disposition, "name")...)
dst.Filename = append(dst.Filename[:0], ContentParam(disposition, "filename")...)
return after + end + 4, nil
}
// NextBody reports how much of data is part content, data[:bodyLen], and where
// what is left begins, data[restOff:], which the caller compacts to the front of
// its buffer before reading more in. done reports the part ended, in which case
// data[restOff:] begins the next part's delimiter; otherwise the bytes past
// bodyLen are a tail held back because it could still turn into a delimiter.
func (m *Multipart) NextBody(data []byte) (bodyLen, restOff int, done bool) {
idx := m.indexPartEnd(data)
if idx >= 0 {
return idx, idx + len("\r\n"), true
}
// Longest prefix of "\r\n--"+boundary that could still be completed.
hold := min(len("\r\n--")+len(m.Boundary)-1, len(data))
return len(data) - hold, len(data) - hold, false
}
// indexDelimiter returns the offset of the leading "--"+Boundary in data.
func (m *Multipart) indexDelimiter(data []byte) int {
for i := 0; i+len("--")+len(m.Boundary) <= len(data); i++ {
dash := bytes.IndexByte(data[i:], '-')
if dash < 0 {
return -1
}
i += dash
if i+len("--")+len(m.Boundary) > len(data) {
return -1
}
if data[i+1] == '-' && b2s(data[i+2:i+2+len(m.Boundary)]) == b2s(m.Boundary) {
return i
}
}
return -1
}
// indexPartEnd returns the offset of the CRLF that closes a part, that is the
// CRLF preceding the next delimiter.
func (m *Multipart) indexPartEnd(data []byte) int {
for i := 0; i+len("\r\n--")+len(m.Boundary) <= len(data); i++ {
cr := bytes.IndexByte(data[i:], '\r')
if cr < 0 {
return -1
}
i += cr
if i+len("\r\n--")+len(m.Boundary) > len(data) {
return -1
}
if data[i+1] == '\n' && data[i+2] == '-' && data[i+3] == '-' &&
b2s(data[i+4:i+4+len(m.Boundary)]) == b2s(m.Boundary) {
return i
}
}
return -1
}
// MediaTypeIs reports whether a Content-Type field value carries the given
// media type, ignoring case and any parameters that follow it, i.e: true for
// "text/plain; charset=utf-8" and media type "text/plain". mediaType must be
// ASCII lowercase. RFC 9110 8.3.1.
func MediaTypeIs(value []byte, mediaType string) bool {
if semi := bytes.IndexByte(value, ';'); semi >= 0 {
value = value[:semi]
}
return equalFold(trimOWS(value), mediaType)
}
// ContentParam returns the value of a parameter of a header field value, i.e:
// "utf-8" for key "charset" of "text/plain; charset=utf-8". Quoted values are
// returned without their quotes and with escapes left as they appear on the
// wire. Key matching is case insensitive, RFC 9110 5.6.6.
func ContentParam(value []byte, key string) []byte {
for len(value) > 0 {
semi := bytes.IndexByte(value, ';')
if semi < 0 {
return nil // No parameters left.
}
value = trimOWS(value[semi+1:])
eq := bytes.IndexByte(value, '=')
if eq < 0 {
return nil
}
gotKey := trimOWS(value[:eq])
value = value[eq+1:]
param := value
if len(param) > 0 && param[0] == '"' {
end := bytes.IndexByte(param[1:], '"')
if end < 0 {
return nil // Unterminated quoted string.
}
param, value = param[1:end+1], param[end+2:]
} else {
end := bytes.IndexByte(param, ';')
if end >= 0 {
param, value = param[:end], param[end:]
} else {
value = nil
}
param = trimOWS(param)
}
if equalFold(gotKey, key) {
return param
}
}
return nil
}
// partField returns the Content-Disposition field value of a part header block.
func partField(partHdr []byte) []byte {
const key = "content-disposition"
for len(partHdr) > 0 {
eol := bytes.IndexByte(partHdr, '\n')
line := partHdr
if eol >= 0 {
line, partHdr = partHdr[:eol], partHdr[eol+1:]
} else {
partHdr = nil
}
colon := bytes.IndexByte(line, ':')
if colon > 0 && equalFold(line[:colon], key) {
return line[colon+1:]
}
}
return nil
}
// trimOWS trims optional whitespace off both ends of b, RFC 9110 5.6.3.
func trimOWS(b []byte) []byte {
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
b = b[1:]
}
for len(b) > 0 && (b[len(b)-1] == ' ' || b[len(b)-1] == '\t') {
b = b[:len(b)-1]
}
return b
}
// equalFold compares b to the ASCII lowercase key, case insensitively.
func equalFold(b []byte, key string) bool {
if len(b) != len(key) {
return false
}
const asciiCapDiff = 'a' - 'A'
for i := range b {
c := b[i]
if c >= 'A' && c <= 'Z' {
c += asciiCapDiff
}
if c != key[i] {
return false
}
}
return true
}
-334
View File
@@ -1,334 +0,0 @@
package httpraw
import (
"io"
"strconv"
"strings"
"testing"
)
// A two part body: a text field and a PNG upload whose bytes contain CRLFs and
// even the boundary text, which must not desync the parser.
const (
multiBoundary = "----abc123"
multiBody = "------abc123\r\n" +
"Content-Disposition: form-data; name=\"caption\"\r\n" +
"\r\n" +
"hi there\r\n" +
"------abc123\r\n" +
"Content-Disposition: form-data; name=\"photo\"; filename=\"beach.png\"\r\n" +
"Content-Type: image/png\r\n" +
"\r\n" +
"\x89PNG\r\n--not-the-boundary\r\n\x00\xff\r\n" +
"------abc123--\r\n"
)
func TestMultipartBoundary(t *testing.T) {
var mp Multipart
for _, test := range []struct {
contentType string
want string
wantErr bool
}{
{contentType: "multipart/form-data; boundary=abc123", want: "abc123"},
{contentType: "multipart/form-data; boundary=\"a b\"", want: "a b"},
{contentType: "multipart/form-data; charset=utf-8; boundary=xyz", want: "xyz"},
{contentType: "multipart/form-data; BOUNDARY=xyz", want: "xyz"}, // Keys are case insensitive.
{contentType: "multipart/form-data", wantErr: true}, // Absent, RFC 2046 5.1.1 requires it.
{contentType: "application/x-www-form-urlencoded", wantErr: true}, // Not multipart at all.
{contentType: "multipart/form-data; boundary=", wantErr: true}, // Empty matches every "--".
} {
err := mp.SetContentType([]byte(test.contentType))
if test.wantErr {
if err == nil {
t.Errorf("%q: want error, got boundary %q", test.contentType, mp.Boundary)
}
continue
} else if err != nil {
t.Errorf("%q: %s", test.contentType, err)
continue
}
got := string(mp.Boundary)
if got != test.want {
t.Errorf("%q: want %q, got %q", test.contentType, test.want, got)
}
}
}
func TestMediaTypeIs(t *testing.T) {
for _, test := range []struct {
value string
media string
want bool
}{
{value: "text/plain", media: "text/plain", want: true},
{value: "text/plain; charset=utf-8", media: "text/plain", want: true},
{value: "text/plain;charset=utf-8", media: "text/plain", want: true},
{value: "Text/Plain", media: "text/plain", want: true}, // Case insensitive, RFC 9110 8.3.1.
{value: " text/plain ; x=1", media: "text/plain", want: true},
{value: "text/plain", media: "text/html"},
{value: "text/plainish", media: "text/plain"}, // Prefix must not match.
{value: "", media: "text/plain"},
{value: "multipart/form-data; boundary=abc", media: "multipart/form-data", want: true},
} {
if got := MediaTypeIs([]byte(test.value), test.media); got != test.want {
t.Errorf("%q is %q: want %v, got %v", test.value, test.media, test.want, got)
}
}
}
func TestContentParam(t *testing.T) {
for _, test := range []struct {
value string
key string
want string
}{
{value: "text/plain; charset=utf-8", key: "charset", want: "utf-8"},
{value: "text/plain;charset=utf-8", key: "charset", want: "utf-8"}, // No space.
{value: "text/plain; charset=\"utf-8\"", key: "charset", want: "utf-8"},
{value: "form-data; name=\"photo\"; filename=\"a;b.png\"", key: "filename", want: "a;b.png"},
{value: "form-data; name=\"photo\"", key: "nope", want: ""},
{value: "form-data; names=x; name=y", key: "name", want: "y"}, // Prefix must not match.
{value: "text/plain", key: "charset", want: ""},
} {
got := ContentParam([]byte(test.value), test.key)
if string(got) != test.want {
t.Errorf("%q key %q: want %q, got %q", test.value, test.key, test.want, got)
}
}
}
func TestNextPartHeader(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
var hdr MultipartHeader
parsed, err := m.NextHeader(&hdr, []byte(multiBody))
if err != nil {
t.Fatal(err)
}
const wantHdr = "Content-Disposition: form-data; name=\"caption\"\r\n"
if string(hdr.PartView) != wantHdr {
t.Errorf("want header %q, got %q", wantHdr, hdr.PartView)
}
if string(hdr.Name) != "caption" {
t.Errorf("want name %q, got %q", "caption", hdr.Name)
}
if len(hdr.Filename) != 0 {
t.Errorf("want no filename for a non file part, got %q", hdr.Filename)
}
if !strings.HasPrefix(multiBody[parsed:], "hi there\r\n") {
t.Errorf("want rest at part body, got %q", multiBody[parsed:])
}
}
// Names and filenames must outlive the buffer they were parsed from, so a
// caller may compact it and read more without losing the part it is reading.
func TestNextPartHeaderOutlivesBuffer(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
data := []byte(multiBody)
var hdr MultipartHeader
if _, err := m.NextHeader(&hdr, data); err != nil {
t.Fatal(err)
}
for i := range data {
data[i] = 'x' // Buffer reused for the next read.
}
if string(hdr.Name) != "caption" {
t.Errorf("want name %q to survive the buffer, got %q", "caption", hdr.Name)
}
}
// Incomplete data must ask for more, never guess.
func TestNextPartHeaderNeedMore(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
for _, data := range []string{
"",
"------abc", // Delimiter cut short.
"------abc123\r\n", // No header block yet.
"------abc123\r\nContent-Disposition: form-", // Header block unterminated.
} {
var hdr MultipartHeader
parsed, err := m.NextHeader(&hdr, []byte(data))
if parsed != 0 || err != nil {
t.Errorf("%q: want (0, nil) asking for more data, got (%d, %v)", data, parsed, err)
}
}
}
// Junk between the delimiter and the part header is a multipart framing error,
// not a header field name error.
func TestNextPartHeaderJunk(t *testing.T) {
m := Multipart{Boundary: []byte("abc")}
var hdr MultipartHeader
if _, err := m.NextHeader(&hdr, []byte("--abcX\r\nA: b\r\n\r\n")); err != errBadDelimiter {
t.Errorf("want errBadDelimiter, got %v", err)
}
}
// The closing delimiter ends iteration.
func TestNextPartHeaderEnd(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
var hdr MultipartHeader
if _, err := m.NextHeader(&hdr, []byte("------abc123--\r\n")); err != io.EOF {
t.Errorf("want io.EOF, got %v", err)
}
}
func TestNextPartBody(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
var hdr MultipartHeader
parsed, err := m.NextHeader(&hdr, []byte(multiBody))
if err != nil {
t.Fatal(err)
}
rest := multiBody[parsed:]
bodyLen, restOff, done := m.NextBody([]byte(rest))
if !done {
t.Fatal("want the part to end within the buffer")
}
if rest[:bodyLen] != "hi there" {
t.Errorf("want body %q, got %q", "hi there", rest[:bodyLen])
}
if !strings.HasPrefix(rest[restOff:], "------abc123\r\n") {
t.Errorf("want rest at next delimiter, got %q", rest[restOff:])
}
}
// A part whose bytes contain CRLFs and boundary-like text must survive intact.
func TestNextPartBodyBinary(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
rest := []byte(multiBody)
var hdr MultipartHeader
parsed, err := m.NextHeader(&hdr, rest) // caption part.
if err != nil {
t.Fatal(err)
}
_, restOff, _ := m.NextBody(rest[parsed:])
rest = rest[parsed+restOff:]
parsed, err = m.NextHeader(&hdr, rest) // photo part.
if err != nil {
t.Fatal(err)
}
rest = rest[parsed:]
bodyLen, restOff, done := m.NextBody(rest)
if !done {
t.Fatal("want the part to end within the buffer")
}
const want = "\x89PNG\r\n--not-the-boundary\r\n\x00\xff"
if string(rest[:bodyLen]) != want {
t.Errorf("want body %q, got %q", want, rest[:bodyLen])
}
if _, err = m.NextHeader(&hdr, rest[restOff:]); err != io.EOF {
t.Errorf("want io.EOF after last part, got %v", err)
}
}
// A delimiter split across two reads must not be mistaken for part data: the
// tail is held back until proven not to be a delimiter.
func TestNextPartBodySplitDelimiter(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
const part = "hi there"
full := part + "\r\n------abc123\r\n"
for split := 1; split < len(full); split++ {
data := full[:split]
bodyLen, restOff, done := m.NextBody([]byte(data))
if done {
continue // Whole delimiter already present, nothing to prove.
}
if bodyLen > len(part) {
t.Fatalf("split %d: emitted %q, past the end of the part", split, data[:bodyLen])
}
if data[:bodyLen]+data[restOff:] != data {
t.Fatalf("split %d: body+rest %q%q does not reconstruct input", split, data[:bodyLen], data[restOff:])
}
}
}
// A file part carries both parameters, and the raw block stays available.
func TestNextHeaderFilePart(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
var hdr MultipartHeader
parsed, err := m.NextHeader(&hdr, []byte(multiBody)) // caption part.
if err != nil {
t.Fatal(err)
}
rest := []byte(multiBody[parsed:])
_, restOff, _ := m.NextBody(rest)
if _, err = m.NextHeader(&hdr, rest[restOff:]); err != nil { // photo part.
t.Fatal(err)
}
if got := string(hdr.Name); got != "photo" {
t.Errorf("want name %q, got %q", "photo", got)
}
if got := string(hdr.Filename); got != "beach.png" {
t.Errorf("want filename %q, got %q", "beach.png", got)
}
if !strings.Contains(string(hdr.PartView), "Content-Type: image/png") {
t.Errorf("want the raw block to hold every field, got %q", hdr.PartView)
}
}
// A failed call must not leave the previous part's fields behind.
func TestNextHeaderResetsOnError(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
var hdr MultipartHeader
if _, err := m.NextHeader(&hdr, []byte(multiBody)); err != nil {
t.Fatal(err)
}
if _, err := m.NextHeader(&hdr, []byte("------abc123--\r\n")); err != io.EOF {
t.Fatalf("want io.EOF, got %v", err)
}
if hdr.PartView != nil || len(hdr.Name) != 0 || len(hdr.Filename) != 0 {
t.Errorf("want cleared header on error, got %+v", hdr)
}
}
// A header reused across parts must stop allocating once its name and filename
// buffers are big enough.
func TestNextHeaderReuseNoAlloc(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
data := []byte(multiBody)
var hdr MultipartHeader
allocs := testing.AllocsPerRun(10, func() {
if _, err := m.NextHeader(&hdr, data); err != nil {
t.Fatal(err)
}
})
if allocs != 0 {
t.Errorf("want a reused header to allocate 0 times, got %v", allocs)
}
}
// The whole loop, as a caller writes it over a buffer it compacts.
func TestMultipartLoop(t *testing.T) {
m := Multipart{Boundary: []byte(multiBoundary)}
rest := []byte(multiBody)
var got []string
var hdr MultipartHeader
for {
parsed, err := m.NextHeader(&hdr, rest)
if err == io.EOF {
break
} else if err != nil {
t.Fatal(err)
} else if parsed == 0 {
t.Fatal("header must complete within the buffer")
}
name := string(hdr.Name)
total := 0
rest = rest[parsed:]
for {
bodyLen, restOff, done := m.NextBody(rest)
total += bodyLen
rest = rest[restOff:]
if done {
break
}
t.Fatal("part must complete within the buffer")
}
got = append(got, name+":"+strconv.Itoa(total))
}
want := "caption:8|photo:28"
if strings.Join(got, "|") != want {
t.Errorf("want %q, got %q", want, strings.Join(got, "|"))
}
}
+290 -98
View File
@@ -3,28 +3,20 @@ package httpraw
import (
"bytes"
"errors"
"slices"
"strconv"
"unsafe"
"github.com/soypat/lneto/internal"
)
var (
errNoProto = errors.New("missing protocol, HTTP/0.9 unsupported")
// ErrNeedMoreData signals a parser was handed an incomplete buffer: append
// more data to it and call again.
ErrNeedMoreData = errors.New("need more data: cannot find trailing lf/delimiter")
errNoBoundary = errors.New("httpraw: multipart boundary not set")
errUnparsed = errors.New("need to finish parsing")
errInvalidName = errors.New("invalid header name")
// ErrBufferExhausted signals a buffer with no room left for the data being
// written and no permission to grow, see [KVBuffer.EnableBufferGrowth].
// Enlarging the buffer handed to Reset is the only fix; a server answers it
// on a request header with 431, RFC 6585 5.
ErrBufferExhausted = errors.New("httpraw: buffer exhausted, increase size")
// ErrHeaderTooMany signals a header block carrying more fields than
// the buffer it is parsed into has room for, see [Header.Reset]. A server
// answers it with 431, RFC 6585 5: no larger buffer is coming, so reading
// the rest of the block would only spend memory on a request already lost.
ErrHeaderTooMany = errors.New("httpraw: more header fields than buffer holds")
errNoProto = errors.New("missing protocol, HTTP/0.9 unsupported")
errNeedMore = errors.New("need more data: cannot find trailing lf")
errUnparsed = errors.New("need to finish parsing")
errInvalidName = errors.New("invalid header name")
errSmallBuffer = errors.New("small read buffer. Increase ReadBufferSize")
errOOM = errors.New("httpraw: buffer out of memory")
// Header.Set and Header.Add mangles the buffer.
// Call them after retrieving the Body. Do not call them before parsing the header (why would you even do that?).
errMangledBuffer = errors.New("httpraw: mangled buffer")
@@ -37,10 +29,6 @@ var (
errBadStatusCodeTxt = errors.New("invalid status code or text")
errCookiesParsed = errors.New("cookies already parsed, reset before parsing again")
errBufferTooLarge = errors.New("httpraw: buffer exceeds max size (offsets are uint16)")
errBadPercentEncode = errors.New("httpraw: invalid percent-encoding in URL")
errBadDelimiter = errors.New("httpraw: junk between multipart delimiter and part")
errNoContentLength = errors.New("httpraw: no Content-Length field")
errBadContentLength = errors.New("httpraw: invalid Content-Length value")
)
// maxBufLen bounds the header buffer. Offsets/lengths are stored as uint16
@@ -49,21 +37,38 @@ var (
const maxBufLen = 0xffff
type headerBuf struct {
kv kvBuffer
// buf[:len] holds entire HTTP header data, which may be normalized by [flags]. buf[off:len] holds data not yet processed during parsing.
// buf []byte
buf []byte
// offset into buf for parsing.
off int
// args contains key-value store.
// headers []argsKV
headers []argsKV
}
// reset sets the buffer data and discards all parsed data. The field table is
// grown to match the new buffer's capacity and never shrinks, so a header
// reused across requests settles on its largest buffer and stops allocating.
func (h *headerBuf) reset(buf []byte, numHeaderCapacity int) {
h.kv.Reset(buf, numHeaderCapacity)
h.off = 0
// reset sets the buffer data and discards all parsed data.
func (h *headerBuf) reset(buf []byte) {
if buf == nil {
buf = h.buf[:0] // Reuse buffer but discard raw data on nil input.
}
if cap(h.headers) == 0 {
h.headers = make([]argsKV, 16)
}
*h = headerBuf{
buf: buf,
headers: h.headers[:0],
}
}
type tokint = uint16
type headerSlice struct {
start tokint
len tokint
}
type argsKV struct {
key headerSlice
value headerSlice // value start >0 means value is present.
}
type scannerState struct {
@@ -88,56 +93,60 @@ func (h *Header) parse(asResponse bool) (err error) {
return err
}
debuglog("http:firstline:done")
err = h.parseNextHeaders(h.Flags())
err = h.parseNextHeaders()
debuglog("http:headers:done")
return err
}
func (h *Header) parseFirstLine(asResponse bool) (err error) {
if len(h.hbuf.kv.buf) > maxBufLen {
if len(h.hbuf.buf) > maxBufLen {
return errBufferTooLarge // Offsets would overflow uint16 tokint.
}
flags := h.Flags()
if asResponse {
h.statusCode, h.statusText, flags, err = h.hbuf.parseFirstLineResponse(flags)
h.statusCode, h.statusText, h.flags, err = h.hbuf.parseFirstLineResponse(h.flags)
} else {
h.method, h.requestTarget, h.proto, flags, err = h.hbuf.parseFirstLineRequest(flags)
h.method, h.requestURI, h.proto, h.flags, err = h.hbuf.parseFirstLineRequest(h.flags)
}
h.hbuf.kv.flags = flags
return err
}
func (h *Header) parseNextHeaders(flags Flags) error {
func (h *Header) parseNextHeaders() error {
var ss scannerState
h.hbuf.parseNextHeaders(&ss, flags)
h.hbuf.parseNextHeaders(&ss)
if ss.err != nil {
h.hbuf.kv.flags |= flagConnClose
h.flags |= flagConnClose
return ss.err
}
h.hbuf.kv.flags |= flagDoneParsingHeader
h.flags |= flagDoneParsingHeader
return nil
}
func (hb *headerBuf) free() int { return hb.kv.free() }
func (hb *headerBuf) readFromBytes(b []byte) {
hb.buf = append(hb.buf, b...)
}
func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) {
func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) }
func (hb *headerBuf) parseNextHeaders(ss *scannerState) {
debuglog("http:nexthdr:loop")
for kv := hb.next(ss); kv.isValidHeader(); kv = hb.next(ss) {
if !hb.kv.canAddOneKV() {
ss.err = ErrHeaderTooMany
for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) {
if len(hb.headers) == cap(hb.headers) {
// Refuse to grow the headers slice: caller must pre-allocate
// sufficient capacity via reset or use a larger initial size.
ss.err = errOOM
return
}
hb.kv.kvs = append(hb.kv.kvs, kv)
hb.headers = append(hb.headers, kv)
}
debuglog("http:nexthdr:done")
}
func (hb *headerBuf) offBuf() []byte {
return hb.kv.buf[hb.off:]
return hb.buf[hb.off:]
}
func (hb *headerBuf) skipLeadingCRLF() {
for hb.off < len(hb.kv.buf) && (hb.kv.buf[hb.off] == '\n' || hb.kv.buf[hb.off] == '\r') {
for hb.off < len(hb.buf) && (hb.buf[hb.off] == '\n' || hb.buf[hb.off] == '\r') {
hb.off++
}
}
@@ -147,7 +156,7 @@ func (hb *headerBuf) scanLine() []byte {
if len(buf) > 0 && buf[len(buf)-1] == '\r' {
buf = buf[:len(buf)-1] // exclude carriage return.
}
if hb.off < len(hb.kv.buf) {
if hb.off < len(hb.buf) {
hb.off++ // consume newline.
}
return buf
@@ -163,17 +172,17 @@ func (hb *headerBuf) scanUntilByte(c byte) []byte {
return buf
}
func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto view, flags Flags, err error) {
func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto headerSlice, flags flags, err error) {
debuglog("http:req:scan")
hb.off = 0 // Parsing first line resets offset.
hb.skipLeadingCRLF()
flags = initFlags
if bytes.IndexByte(hb.offBuf(), '\n') < 0 {
return method, uri, proto, flags, ErrNeedMoreData // Incomplete line.
return method, uri, proto, flags, errNeedMore // Incomplete line.
}
b := hb.scanLine()
if len(b) < 5 {
return method, uri, proto, flags, ErrNeedMoreData
return method, uri, proto, flags, errNeedMore
}
debuglog("http:req:parse")
@@ -181,8 +190,8 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto
reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ')
if reqURIEnd > 0 {
reqURIEnd += methodEnd + 1
uri = hb.kv.view(b[methodEnd+1 : reqURIEnd])
proto = hb.kv.view(b[reqURIEnd+1:]) // Skip space before protocol.
uri = hb.slice(b[methodEnd+1 : reqURIEnd])
proto = hb.slice(b[reqURIEnd+1:]) // Skip space before protocol.
if b2s(b[reqURIEnd+1:]) != strHTTP11 {
flags |= flagNoHTTP11
}
@@ -191,30 +200,30 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto
} else {
// No version provided.
flags |= flagNoHTTP11
uri = hb.kv.view(b[methodEnd+1:])
uri = hb.slice(b[methodEnd+1:])
}
method = hb.kv.view(b[:methodEnd])
method = hb.slice(b[:methodEnd])
return method, uri, proto, flags, nil
}
func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, statusText view, flags Flags, err error) {
func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, statusText headerSlice, flags flags, err error) {
debuglog("http:resp:scan")
hb.off = 0 // Parsing first line resets offset.
hb.skipLeadingCRLF()
flags = initFlags
if bytes.IndexByte(hb.offBuf(), '\n') < 0 {
return statusCode, statusText, flags, ErrNeedMoreData // Incomplete line.
return statusCode, statusText, flags, errNeedMore // Incomplete line.
}
b := hb.scanLine()
if len(b) < 5 {
return statusCode, statusText, flags, ErrNeedMoreData
return statusCode, statusText, flags, errNeedMore
}
debuglog("http:resp:parse")
// Parse protocol (e.g. "HTTP/1.1"), then status code, then status text.
protoEnd := bytes.IndexByte(b, ' ')
if protoEnd < 0 {
return statusCode, statusText, flags, ErrNeedMoreData
return statusCode, statusText, flags, errNeedMore
}
if b2s(b[:protoEnd]) != strHTTP11 {
flags |= flagNoHTTP11
@@ -235,27 +244,208 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, status
return statusCode, statusText, flags, errBadStatusCode
}
}
statusCode = hb.kv.view(code)
statusCode = hb.slice(code)
if codeEnd < len(b) {
statusText = hb.kv.view(b[codeEnd+1:]) // Skip space before text.
statusText = hb.slice(b[codeEnd+1:]) // Skip space before text.
}
debuglog("http:resp:done")
return statusCode, statusText, flags, nil
}
func (hb *headerBuf) next(ss *scannerState) pairKV {
func (kv argsKV) isValid() bool {
return kv.key.start > 0
}
func (kv *argsKV) invalidate() {
*kv = argsKV{}
}
func (tb headerBuf) musttoken(slice headerSlice) []byte {
return tok2bytes(tb.buf, slice)
}
func (tb headerBuf) slice(b []byte) headerSlice {
return bytes2tok(tb.buf, b)
}
func (kv argsKV) HasValue() bool { return kv.value.start > 0 }
func (h *Header) hasHeaderValue(key, value string) bool {
kv := h.peekHeader(key)
return kv.isValid() && b2s(h.hbuf.musttoken(kv.value)) == value
}
// peekHeader returns header key-value for the given key.
//
// The returned value is valid until the request is released,
// either though ReleaseRequest or your request handler returning.
// Do not store references to returned value. Make copies instead.
func (h *Header) peekHeader(key string) argsKV {
hb := &h.hbuf
for i := 0; i < len(h.hbuf.headers); i++ {
if b2s(hb.musttoken(h.hbuf.headers[i].key)) == key {
return h.hbuf.headers[i]
}
}
return hb.noKV()
}
func (hb *headerBuf) mustAppendSlice(value string) headerSlice {
L := len(hb.buf)
if L == 0 {
L++ // Valid key-values start after 0.
}
copy(hb.buf[L:L+len(value)], value)
hb.buf = hb.buf[:L+len(value)]
return hb.slice(hb.buf[L : L+len(value)])
}
func (h *Header) reuseOrAppend(tok headerSlice, value string) headerSlice {
if tok.len > tokint(len(value)) {
copy(h.hbuf.musttoken(tok), value)
tok.len = tokint(len(value))
return tok
}
return h.appendSlice(value)
}
func (h *Header) appendSlice(value string) headerSlice {
debuglog("http:appendslice:start")
if !h.reserve(len(value)) {
return headerSlice{}
}
h.flags |= flagMangledBuffer
return h.hbuf.mustAppendSlice(value)
}
func (h *Header) appendHeader(key, value string) {
// reserve accounts for the byte-0 reservation mustAppendSlice makes on an
// empty buffer, and drops (flagging OOM) rather than panicking when growth
// is disabled and space runs out.
if !h.reserve(len(key) + len(value)) {
return
}
h.flags |= flagMangledBuffer
hb := &h.hbuf
k := hb.mustAppendSlice(key)
v := hb.mustAppendSlice(value)
debuglog("http:appendhdr:grow-hdrs")
hb.headers = append(hb.headers, argsKV{
key: k,
value: v,
})
}
// appendHeaderInt is appendHeader's integer counterpart: it appends key and the
// formatted integer value as a new header field.
func (h *Header) appendHeaderInt(key string, value int64, base int) {
n := intLen(value, base)
if !h.reserve(len(key) + n) {
return // Drop and flag OOM; never panic.
}
h.flags |= flagMangledBuffer
hb := &h.hbuf
k := hb.mustAppendSlice(key)
v := hb.mustAppendInt(value, base)
hb.headers = append(hb.headers, argsKV{
key: k,
value: v,
})
}
// reserve ensures need free bytes are available in the buffer, growing it when
// permitted. It accounts for the byte-0 reservation on an empty buffer (see
// mustAppendSlice). It returns false and sets flagOOMReached when the space
// cannot be guaranteed: a tokint offset overflow, or a full buffer with
// flagNoBufferGrow set.
func (h *Header) reserve(need int) bool {
hb := &h.hbuf
if len(hb.buf) == 0 {
need++ // mustAppend* reserves byte 0 on an empty buffer.
}
if len(hb.buf)+need > maxBufLen {
h.flags |= flagOOMReached // Offsets would overflow uint16 tokint.
return false
}
if need > hb.free() {
if h.flags.hasAny(flagNoBufferGrow) {
h.flags |= flagOOMReached
return false
}
hb.buf = slices.Grow(hb.buf, need)
}
return true
}
// reuseOrAppendInt writes value into tok's slot in place when it fits, avoiding
// any buffer growth; otherwise it appends a fresh slot.
func (h *Header) reuseOrAppendInt(tok headerSlice, value int64, base int) headerSlice {
n := intLen(value, base)
if int(tok.len) >= n {
// Reuse: format directly over the existing slot. No free space needed
// since n <= tok.len and the slot already lives inside buf.
v := strconv.AppendInt(h.hbuf.buf[tok.start:tok.start], value, base)
tok.len = tokint(len(v))
h.flags |= flagMangledBuffer
return tok
}
return h.appendInt(value, base, n)
}
// appendInt reserves space (growing or flagging OOM) and appends value as a new slot.
func (h *Header) appendInt(value int64, base, n int) headerSlice {
if !h.reserve(n) {
return headerSlice{} // Drop and flag OOM; never panic.
}
h.flags |= flagMangledBuffer
return h.hbuf.mustAppendInt(value, base)
}
// mustAppendInt formats value into the buffer's free region and commits it.
// The caller must have reserved at least intLen(value, base) free bytes.
func (hb *headerBuf) mustAppendInt(value int64, base int) headerSlice {
L := len(hb.buf)
if L == 0 {
L++ // Valid key-values start after byte 0.
}
v := strconv.AppendInt(hb.buf[L:L], value, base)
hb.buf = hb.buf[:L+len(v)]
return hb.slice(hb.buf[L : L+len(v)])
}
// intLen returns the number of bytes strconv.AppendInt would emit for value in
// the given base (including a leading minus sign for negatives). Used to size
// the buffer and to test whether a value fits an existing slot without writing.
func intLen(value int64, base int) int {
n := 1
u := uint64(value)
if value < 0 {
n++ // Leading minus sign.
u = -u // Two's-complement magnitude; correct even for math.MinInt64.
}
for u >= uint64(base) {
u /= uint64(base)
n++
}
return n
}
func (hb *headerBuf) noKV() argsKV { return argsKV{} }
func (hb *headerBuf) next(ss *scannerState) argsKV {
if !ss.initialized {
ss.nextColon = -1
ss.nextNewLine = -1
}
buf := hb.kv.buf[hb.off:]
buf := hb.buf[hb.off:]
blen := len(buf)
if blen >= 2 && buf[0] == '\r' && buf[1] == '\n' {
hb.off += 2
return hb.kv.noKV() // \r\n\r\n Ends header.
return hb.noKV() // \r\n\r\n Ends header.
} else if blen >= 1 && buf[0] == '\n' {
hb.off += 1
return hb.kv.noKV() // \n\n Ends header.
return hb.noKV() // \n\n Ends header.
}
// n is parsing offset. Will start by storing colon index.
@@ -270,19 +460,19 @@ func (hb *headerBuf) next(ss *scannerState) pairKV {
if x < 0 {
// A header name should always at some point be followed by a \n
// even if it's the one that terminates the header block.
ss.err = ErrNeedMoreData
return hb.kv.noKV()
ss.err = errNeedMore
return hb.noKV()
} else if x < n {
// There was a \n before the colon! This is invalid.
ss.err = errInvalidName
return hb.kv.noKV()
return hb.noKV()
} else if n < 0 {
// A newline is present (x>=0 reached here) but the line has no
// colon: malformed, not incomplete. A split arriving before the
// colon has no newline yet and is caught by the x<0 branch above,
// so it still returns ErrNeedMoreData.
// so it still returns errNeedMore.
ss.err = errInvalidName
return hb.kv.noKV()
return hb.noKV()
}
}
// n stores colon position by now.
@@ -290,12 +480,12 @@ func (hb *headerBuf) next(ss *scannerState) pairKV {
// Spaces between the header key and colon are not allowed.
// See RFC 7230, Section 3.2.4.
ss.err = errInvalidName
return hb.kv.noKV()
return hb.noKV()
}
// Ready to store key..
var resultKV pairKV
resultKV.key = hb.kv.view(buf[:n])
var resultKV argsKV
resultKV.key = hb.slice(buf[:n])
n++ // consume colon.
for len(buf) > n && buf[n] == ' ' {
n++ // Trim leading spaces.
@@ -308,8 +498,8 @@ func (hb *headerBuf) next(ss *scannerState) pairKV {
nl := bytes.IndexByte(buf[n:], '\n')
if nl < 0 || nl+n+1 == len(buf) {
// No newline or newline is last character and can't know if is multiline.
ss.err = ErrNeedMoreData
return hb.kv.noKV()
ss.err = errNeedMore
return hb.noKV()
}
n += nl + 1 // Index of the newly found newline.
nextChar := buf[n]
@@ -322,40 +512,42 @@ func (hb *headerBuf) next(ss *scannerState) pairKV {
if valueEnd > valueStart && buf[valueEnd-1] == '\r' {
valueEnd-- // Trim \r character if present before value.
}
resultKV.value = hb.kv.view(buf[valueStart:valueEnd])
resultKV.value = hb.slice(buf[valueStart:valueEnd])
hb.off += n
return resultKV
}
// ConnectionClose returns true if 'Connection: close' header is set or if a invalid header was found.
func (h *Header) ConnectionClose() bool {
flags := h.Flags()
closed := flags.HasAny(flagConnClose) ||
h.hasConnectionToken(strClose) ||
(flags.HasAny(flagNoHTTP11) && !h.hasConnectionToken(strKeepAlive))
closed := h.flags.hasAny(flagConnClose) ||
h.hasHeaderValue(headerConnection, strClose) ||
(h.flags.hasAny(flagNoHTTP11) && !h.hasHeaderValue(headerConnection, "keep-alive"))
if closed {
h.hbuf.kv.flags |= flagConnClose
h.flags |= flagConnClose
}
return closed
}
// hasConnectionToken reports whether the Connection field lists token, which
// must be lowercase. The field name, its comma list and each token all compare
// case insensitively, RFC 9110 5.1 and 7.6.1.
func (h *Header) hasConnectionToken(token string) bool {
value := h.GetFold(headerConnection)
for len(value) > 0 {
item := value
if comma := bytes.IndexByte(value, ','); comma >= 0 {
item, value = value[:comma], value[comma+1:]
} else {
value = nil
}
if equalFold(trimOWS(item), token) {
return true
}
// b2s converts byte slice to a string without memory allocation.
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
func b2s(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
func tok2bytes(buf []byte, slice headerSlice) []byte {
return buf[slice.start : slice.start+slice.len]
}
func bytes2tok(buf, value []byte) headerSlice {
base := uintptr(unsafe.Pointer(unsafe.SliceData(buf)))
off := uintptr(unsafe.Pointer(unsafe.SliceData(value)))
if off < base || off > base+uintptr(len(buf)) {
panic("httpx: argument buffer does not alias header buffer")
}
return headerSlice{
start: tokint(off - base),
len: tokint(len(value)),
}
return false
}
const enableDebug = internal.HeapAllocDebugging
+36 -75
View File
@@ -10,17 +10,20 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
// Full HTTP request split across multiple ReadFromBytes calls.
full := "GET /index.html HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/html\r\n\r\nbody here"
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
// Feed data in small chunks to exercise incremental parsing.
chunks := splitInto(full, 10)
var done bool
var doneIdx int
for i, chunk := range chunks {
err := hdr.ReadFromBytes([]byte(chunk))
n, err := hdr.ReadFromBytes([]byte(chunk))
if err != nil {
t.Fatalf("ReadFromBytes: %v", err)
}
if n != len(chunk) {
t.Fatalf("expected %d bytes read, got %d", len(chunk), n)
}
var needMore bool
needMore, err = hdr.TryParse(false)
@@ -49,16 +52,19 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
if string(hdr.Method()) != "GET" {
t.Errorf("method = %q; want GET", hdr.Method())
}
if string(hdr.RequestTarget()) != "/index.html" {
t.Errorf("URI = %q; want /index.html", hdr.RequestTarget())
if string(hdr.RequestURI()) != "/index.html" {
t.Errorf("URI = %q; want /index.html", hdr.RequestURI())
}
// Verify headers via ForEach.
headers := make(map[string]string)
hdr.ForEach(func(key, value []byte) bool {
err := hdr.ForEach(func(key, value []byte) error {
headers[string(key)] = string(value)
return true
return nil
})
if err != nil {
t.Fatal(err)
}
if headers["Host"] != "example.com" {
t.Errorf("Host = %q; want example.com", headers["Host"])
}
@@ -79,7 +85,7 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
func TestTryParse_IncrementalResponse(t *testing.T) {
full := "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nServer: lneto\r\n\r\nhello"
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
chunks := splitInto(full, 8)
var done bool
@@ -131,7 +137,7 @@ func TestReadFromLimited(t *testing.T) {
r := strings.NewReader(data)
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
// Read in one shot.
n, err := hdr.ReadFromLimited(r, 256)
@@ -157,7 +163,7 @@ func TestReadFromLimited(t *testing.T) {
func TestReadFromLimited_MaxBytes(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
// Zero maxBytesToRead should error.
_, err := hdr.ReadFromLimited(strings.NewReader("data"), 0)
@@ -168,9 +174,9 @@ func TestReadFromLimited_MaxBytes(t *testing.T) {
func TestReadFromBytes_Empty(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
err := hdr.ReadFromBytes(nil)
_, err := hdr.ReadFromBytes(nil)
if err == nil {
t.Fatal("expected error for empty bytes")
}
@@ -178,7 +184,7 @@ func TestReadFromBytes_Empty(t *testing.T) {
func TestBufferFreeAndCapacity(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 100), defaultKVCap)
hdr.Reset(make([]byte, 0, 100))
if hdr.BufferCapacity() != 100 {
t.Errorf("capacity = %d; want 100", hdr.BufferCapacity())
@@ -196,14 +202,15 @@ func TestBufferFreeAndCapacity(t *testing.T) {
func TestEnableBufferGrowth(t *testing.T) {
var hdr Header
buf := make([]byte, 0, 64)
hdr.Reset(buf, defaultKVCap)
hdr.ConfigBufferGrowth(false)
hdr.Reset(buf)
hdr.EnableBufferGrowth(false)
// With growth disabled, reading more than capacity should fail.
big := make([]byte, 128)
for i := range big {
big[i] = 'A'
}
err := hdr.ReadFromBytes(big)
_, err := hdr.ReadFromBytes(big)
if err == nil {
t.Fatal("expected error when buffer growth disabled and data exceeds capacity")
}
@@ -222,11 +229,11 @@ func TestHeader_Add(t *testing.T) {
// ForEach should find both.
var values []string
hdr.ForEach(func(key, value []byte) bool {
hdr.ForEach(func(key, value []byte) error {
if string(key) == "X-Custom" {
values = append(values, string(value))
}
return true
return nil
})
if len(values) != 2 {
t.Fatalf("expected 2 X-Custom headers, got %d", len(values))
@@ -325,14 +332,6 @@ func TestCookie_ParseBytes(t *testing.T) {
if string(c.Get("Path")) != "/" {
t.Errorf("Path = %q; want /", c.Get("Path"))
}
// The first pair sits at buffer offset 0, which a presence check keyed on
// the offset rather than the length reads as absent.
if string(c.Get("session")) != "abc123" {
t.Errorf("Get(session) = %q; want abc123", c.Get("session"))
}
if !c.HasKeyOrSingleValue("session") {
t.Error("expected first pair to be present by key")
}
if !c.HasKeyOrSingleValue("Secure") {
t.Error("expected Secure flag")
}
@@ -358,10 +357,13 @@ func TestCookie_ForEach(t *testing.T) {
c.ParseBytes([]byte("a=1; b=2; c=3"))
var keys []string
c.ForEach(func(key, value []byte) bool {
err := c.ForEach(func(key, value []byte) error {
keys = append(keys, string(key))
return true
return nil
})
if err != nil {
t.Fatal(err)
}
if len(keys) != 3 {
t.Fatalf("expected 3 cookie entries, got %d", len(keys))
}
@@ -387,7 +389,7 @@ func TestHeader_MultilineValue(t *testing.T) {
func TestHeader_ResponseRoundTrip(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
hdr.SetProtocol("HTTP/1.1")
hdr.SetStatus("404", "Not Found")
hdr.Add("Content-Type", "text/plain")
@@ -427,10 +429,10 @@ func TestHeader_ResponseRoundTrip(t *testing.T) {
func TestHeader_RequestRoundTrip(t *testing.T) {
var hdr Header
hdr.Reset(make([]byte, 0, 256), defaultKVCap)
hdr.Reset(make([]byte, 0, 256))
hdr.SetProtocol("HTTP/1.1")
hdr.SetMethod("POST")
hdr.SetRequestTarget("/api/data")
hdr.SetRequestURI("/api/data")
hdr.Add("Host", "example.com")
hdr.Add("Content-Type", "application/json")
@@ -452,8 +454,8 @@ func TestHeader_RequestRoundTrip(t *testing.T) {
if string(hdr2.Method()) != "POST" {
t.Errorf("re-parsed method = %q; want POST", hdr2.Method())
}
if string(hdr2.RequestTarget()) != "/api/data" {
t.Errorf("re-parsed URI = %q; want /api/data", hdr2.RequestTarget())
if string(hdr2.RequestURI()) != "/api/data" {
t.Errorf("re-parsed URI = %q; want /api/data", hdr2.RequestURI())
}
if string(hdr2.Get("Host")) != "example.com" {
t.Errorf("re-parsed Host = %q; want example.com", hdr2.Get("Host"))
@@ -502,8 +504,8 @@ func TestParseRequest_NoProtocol(t *testing.T) {
if string(hdr.Method()) != "GET" {
t.Errorf("method = %q; want GET", hdr.Method())
}
if string(hdr.RequestTarget()) != "/simple" {
t.Errorf("URI = %q; want /simple", hdr.RequestTarget())
if string(hdr.RequestURI()) != "/simple" {
t.Errorf("URI = %q; want /simple", hdr.RequestURI())
}
if hdr.Protocol() != nil {
t.Errorf("protocol should be nil for version-less request, got %q", hdr.Protocol())
@@ -537,44 +539,3 @@ func splitInto(s string, n int) []string {
}
return chunks
}
// Connection is a case-insensitive list of case-insensitive tokens, RFC 9110
// 7.6.1, and its field name folds like any other, RFC 9110 5.1. Missing a close
// token keeps serving a peer that asked to hang up; missing keep-alive hangs up
// on an HTTP/1.0 peer that asked to stay.
func TestConnectionCloseFolded(t *testing.T) {
for _, test := range []struct {
proto string
field string
wantClose bool
}{
{proto: "HTTP/1.1", field: "Connection: close", wantClose: true},
{proto: "HTTP/1.1", field: "connection: close", wantClose: true},
{proto: "HTTP/1.1", field: "CONNECTION: close", wantClose: true},
{proto: "HTTP/1.1", field: "Connection: Close", wantClose: true},
{proto: "HTTP/1.1", field: "Connection: CLOSE", wantClose: true},
{proto: "HTTP/1.1", field: "Connection: keep-alive, close", wantClose: true},
{proto: "HTTP/1.1", field: "Connection: close, keep-alive", wantClose: true},
{proto: "HTTP/1.1", field: "Connection: TE, Close", wantClose: true},
// A token that merely contains "close" is not the close token.
{proto: "HTTP/1.1", field: "Connection: closed", wantClose: false},
{proto: "HTTP/1.1", field: "Connection: keep-alive", wantClose: false},
// HTTP/1.0 closes unless the peer asks to keep the connection.
{proto: "HTTP/1.0", field: "Connection: keep-alive", wantClose: false},
{proto: "HTTP/1.0", field: "connection: keep-alive", wantClose: false},
{proto: "HTTP/1.0", field: "Connection: Keep-Alive", wantClose: false},
{proto: "HTTP/1.0", field: "Connection: TE, keep-alive", wantClose: false},
{proto: "HTTP/1.0", field: "Host: h", wantClose: true},
} {
t.Run(test.proto+" "+test.field, func(t *testing.T) {
var hdr Header
full := "GET / " + test.proto + "\r\nHost: h\r\n" + test.field + "\r\n\r\n"
if err := hdr.ParseBytes(false, []byte(full)); err != nil {
t.Fatal(err)
}
if got := hdr.ConnectionClose(); got != test.wantClose {
t.Errorf("want ConnectionClose=%v, got %v", test.wantClose, got)
}
})
}
}
-19
View File
@@ -1,19 +0,0 @@
package internal
// IntLen returns the number of bytes [strconv.AppendInt] emits for value in the
// given base, including a leading minus sign for negatives. Lets callers size a
// buffer, or test whether a value fits an existing slot, before writing a byte.
// base must be in the range 2..36, as accepted by [strconv.AppendInt].
func IntLen(value int64, base int) int {
n := 1
u := uint64(value)
if value < 0 {
n++ // Leading minus sign.
u = -u // Two's-complement magnitude; correct even for math.MinInt64.
}
for u >= uint64(base) {
u /= uint64(base)
n++
}
return n
}
+2 -7
View File
@@ -768,11 +768,6 @@ func httpBodyClass(contentType, body []byte) FieldClass {
return FieldClassText
}
// maxCapturedHeaderFields bounds the field table a captured HTTP header is
// parsed into. Captures are read once and discarded, so the table is sized for
// a realistic request rather than for whatever the capture happens to hold.
const maxCapturedHeaderFields = 64
func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
debuglog("pcap:http:start")
const httpProtocol = "HTTP"
@@ -782,10 +777,10 @@ func (pc *PacketBreakdown) CaptureHTTP(dst []Frame, pkt []byte, bitOffset int) (
const asResponse = true
const asRequest = false
httpData := pkt[bitOffset/8:]
pc.hdr.Reset(httpData, maxCapturedHeaderFields)
pc.hdr.Reset(httpData)
err := pc.hdr.Parse(asResponse)
if err != nil {
pc.hdr.Reset(httpData, 0) // Field table already sized, reuse it.
pc.hdr.Reset(httpData)
err = pc.hdr.Parse(asRequest) // try as request.
}
if err != nil {
+2 -3
View File
@@ -33,9 +33,8 @@ const _FieldClass_name = "undefinedsourcedestinationprotocolversiontypesizeflags
var _FieldClass_index = [...]uint8{0, 9, 15, 26, 34, 41, 45, 49, 54, 68, 76, 83, 90, 94, 101, 112, 114, 123, 131}
func (i FieldClass) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_FieldClass_index)-1 {
if i >= FieldClass(len(_FieldClass_index)-1) {
return "FieldClass(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _FieldClass_name[_FieldClass_index[idx]:_FieldClass_index[idx+1]]
return _FieldClass_name[_FieldClass_index[i]:_FieldClass_index[i+1]]
}
+4 -6
View File
@@ -66,11 +66,10 @@ const _CodeDestinationUnreachable_name = "net unreachablehost unreachableprotoco
var _CodeDestinationUnreachable_index = [...]uint8{0, 15, 31, 51, 67, 98, 117}
func (i CodeDestinationUnreachable) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CodeDestinationUnreachable_index)-1 {
if i >= CodeDestinationUnreachable(len(_CodeDestinationUnreachable_index)-1) {
return "CodeDestinationUnreachable(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[idx]:_CodeDestinationUnreachable_index[idx+1]]
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[i]:_CodeDestinationUnreachable_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -87,9 +86,8 @@ const _CodeRedirect_name = "redirect for networkredirect for hostredirect for To
var _CodeRedirect_index = [...]uint8{0, 20, 37, 61, 82}
func (i CodeRedirect) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CodeRedirect_index)-1 {
if i >= CodeRedirect(len(_CodeRedirect_index)-1) {
return "CodeRedirect(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CodeRedirect_name[_CodeRedirect_index[idx]:_CodeRedirect_index[idx+1]]
return _CodeRedirect_name[_CodeRedirect_index[i]:_CodeRedirect_index[i+1]]
}
+4 -6
View File
@@ -66,11 +66,10 @@ const _CodeDestinationUnreachable_name = "no route to destinationcommunication a
var _CodeDestinationUnreachable_index = [...]uint8{0, 23, 64, 94, 113, 129, 172, 199}
func (i CodeDestinationUnreachable) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CodeDestinationUnreachable_index)-1 {
if i >= CodeDestinationUnreachable(len(_CodeDestinationUnreachable_index)-1) {
return "CodeDestinationUnreachable(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[idx]:_CodeDestinationUnreachable_index[idx+1]]
return _CodeDestinationUnreachable_name[_CodeDestinationUnreachable_index[i]:_CodeDestinationUnreachable_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
@@ -86,9 +85,8 @@ const _CodeParameterProblem_name = "erroneous header field encounteredunrecogniz
var _CodeParameterProblem_index = [...]uint8{0, 34, 75, 111}
func (i CodeParameterProblem) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CodeParameterProblem_index)-1 {
if i >= CodeParameterProblem(len(_CodeParameterProblem_index)-1) {
return "CodeParameterProblem(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CodeParameterProblem_name[_CodeParameterProblem_index[idx]:_CodeParameterProblem_index[idx+1]]
return _CodeParameterProblem_name[_CodeParameterProblem_index[i]:_CodeParameterProblem_index[i+1]]
}
+2 -3
View File
@@ -29,9 +29,8 @@ const _LinkMode_name = "down10M-H10M-F100M-H100M-F100M-T41000M-H1000M-F2.5G-F5G-
var _LinkMode_index = [...]uint8{0, 4, 9, 14, 20, 26, 33, 40, 47, 53, 57, 62, 67, 72, 78}
func (i LinkMode) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_LinkMode_index)-1 {
if i >= LinkMode(len(_LinkMode_index)-1) {
return "LinkMode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _LinkMode_name[_LinkMode_index[idx]:_LinkMode_index[idx+1]]
return _LinkMode_name[_LinkMode_index[i]:_LinkMode_index[i+1]]
}
+3 -4
View File
@@ -212,17 +212,16 @@ func _() {
_ = x[ErrAlreadyRegistered-17]
_ = x[ErrTruncatedFrame-18]
_ = x[ErrMissingHALConfig-19]
_ = x[ErrBadState-20]
}
const (
_errGeneric_name_0 = "lneto-bug(use build tag \"debugheaplog\")packet droppedincorrect checksumzero source(port/addr)zero destination(port/addr)short bufferbuffer fullinvalid addressunsupportedmismatchmismatched lengthinvalid configuration"
_errGeneric_name_1 = "invalid fieldinvalid length fieldresource exhaustedprotocol already registeredtruncated framemissing HAL configurationoperation invalid in current state"
_errGeneric_name_1 = "invalid fieldinvalid length fieldresource exhaustedprotocol already registeredtruncated framemissing HAL configuration"
)
var (
_errGeneric_index_0 = [...]uint8{0, 39, 53, 71, 93, 120, 132, 143, 158, 169, 177, 194, 215}
_errGeneric_index_1 = [...]uint8{0, 13, 33, 51, 78, 93, 118, 152}
_errGeneric_index_1 = [...]uint8{0, 13, 33, 51, 78, 93, 118}
)
func (i errGeneric) String() string {
@@ -230,7 +229,7 @@ func (i errGeneric) String() string {
case 1 <= i && i <= 12:
i -= 1
return _errGeneric_name_0[_errGeneric_index_0[i]:_errGeneric_index_0[i+1]]
case 14 <= i && i <= 20:
case 14 <= i && i <= 19:
i -= 14
return _errGeneric_name_1[_errGeneric_index_1[i]:_errGeneric_index_1[i+1]]
default:
+2 -3
View File
@@ -26,11 +26,10 @@ const _State_name = "CLOSEDLISTENSYN-RECEIVEDSYN-SENTESTABLISHEDFIN-WAIT-1FIN-WA
var _State_index = [...]uint8{0, 6, 12, 24, 32, 43, 53, 63, 70, 79, 89, 97}
func (i State) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_State_index)-1 {
if i >= State(len(_State_index)-1) {
return "State(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _State_name[_State_index[idx]:_State_index[idx+1]]
return _State_name[_State_index[i]:_State_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
-11
View File
@@ -1,11 +0,0 @@
// Package rawsock listens and reads over Linux sockets through syscalls
// directly, without the net package. It exists so a server can be measured
// without net.TCPConn, its netFD and its poller on the path.
//
// [Conn] and [Listener] implement [net.Conn] and [net.Listener] the way
// TinyGo's net package does: a connection is its file descriptor plus its
// addresses, deadlines live on the struct and are handed to the socket, and
// nothing sits between a Read and the syscall. A server written against these
// interfaces runs unchanged over the net package, over this package and over
// TinyGo's netdev.
package rawsock
-7
View File
@@ -1,7 +0,0 @@
//go:build !tinygo && darwin
package rawsock
import "syscall"
const sysaccept = syscall.SYS_ACCEPT
-7
View File
@@ -1,7 +0,0 @@
//go:build !tinygo && linux
package rawsock
import "syscall"
const sysaccept = syscall.SYS_ACCEPT4
-49
View File
@@ -1,49 +0,0 @@
//go:build tinygo || (!linux && !darwin)
package rawsock
import (
"errors"
"net"
"net/netip"
"time"
)
var (
_ net.Conn = (*Conn)(nil)
_ net.Listener = (*Listener)(nil)
)
// Addr is a socket address implementing [net.Addr].
type Addr netip.AddrPort
func (a Addr) Network() string { return "tcp" }
func (a Addr) String() string { return netip.AddrPort(a).String() }
func (a Addr) AddrPort() netip.AddrPort { return netip.AddrPort(a) }
// Conn is an accepted connection. Unsupported on this platform.
type Conn struct{}
func (c *Conn) Read(b []byte) (int, error) { return 0, errors.ErrUnsupported }
func (c *Conn) Write(b []byte) (int, error) { return 0, errors.ErrUnsupported }
func (c *Conn) Close() error { return errors.ErrUnsupported }
func (c *Conn) LocalAddr() net.Addr { return Addr{} }
func (c *Conn) RemoteAddr() net.Addr { return Addr{} }
func (c *Conn) SetDeadline(t time.Time) error { return errors.ErrUnsupported }
func (c *Conn) SetReadDeadline(t time.Time) error { return errors.ErrUnsupported }
func (c *Conn) SetWriteDeadline(t time.Time) error { return errors.ErrUnsupported }
func (c *Conn) SetReadTimeout(timeout time.Duration) error { return errors.ErrUnsupported }
func (c *Conn) SetWriteTimeout(timeout time.Duration) error { return errors.ErrUnsupported }
// Listener is a listening socket. Unsupported on this platform.
type Listener struct{}
// Listen is unsupported on this platform.
func (l *Listener) Listen(port uint16) error { return errors.ErrUnsupported }
func (l *Listener) Accept() (net.Conn, error) { return nil, errors.ErrUnsupported }
func (l *Listener) AcceptConn(conn *Conn) error { return errors.ErrUnsupported }
func (l *Listener) Addr() net.Addr { return Addr{} }
func (l *Listener) Port() uint16 { return 0 }
func (l *Listener) Close() error { return errors.ErrUnsupported }
-244
View File
@@ -1,244 +0,0 @@
//go:build !tinygo && (linux || darwin)
package rawsock
import (
"net"
"net/netip"
"syscall"
"time"
"unsafe"
)
// The interfaces this package exists to satisfy: an http.Server and a heapless
// router must both accept what Listen hands back.
var (
_ net.Conn = (*Conn)(nil)
_ net.Listener = (*Listener)(nil)
)
// Addr is a socket address. It implements [net.Addr] over a
// [netip.AddrPort], which costs no allocation to carry around, unlike
// [net.TCPAddr] and its IP slice.
type Addr netip.AddrPort
// Network returns "tcp".
func (a Addr) Network() string { return "tcp" }
// String returns the address in host:port form.
func (a Addr) String() string { return netip.AddrPort(a).String() }
// AddrPort returns the address as a [netip.AddrPort].
func (a Addr) AddrPort() netip.AddrPort { return netip.AddrPort(a) }
// Conn is an accepted TCP connection over a raw socket file descriptor. It
// implements [net.Conn], so both an [net/http.Server] and a heapless router can
// be handed the same connection.
//
// Deadlines are enforced by the socket itself through SO_RCVTIMEO and
// SO_SNDTIMEO: a read that runs out of time fails with a [net.Error] that
// reports Timeout, as callers of net.Conn expect.
type Conn struct {
fd int
local, remote Addr
readDeadline time.Time
writeDeadline time.Time
}
// Read reads bytes from the connection into b.
func (c *Conn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
n, err := syscall.Read(c.fd, b)
if err != nil {
return 0, c.opError("read", err)
}
if n == 0 {
return 0, syscall.ECONNRESET // Peer closed.
}
return n, nil
}
// Write writes b to the connection, looping until all bytes are sent.
func (c *Conn) Write(b []byte) (int, error) {
total := 0
for total < len(b) {
n, err := syscall.Write(c.fd, b[total:])
if err != nil {
return total, c.opError("write", err)
}
total += n
}
return total, nil
}
// Close closes the underlying file descriptor.
func (c *Conn) Close() error {
return syscall.Close(c.fd)
}
// LocalAddr returns the address the connection was accepted on.
func (c *Conn) LocalAddr() net.Addr { return c.local }
// RemoteAddr returns the peer address of the connection.
func (c *Conn) RemoteAddr() net.Addr { return c.remote }
// SetDeadline sets both the read and write deadline. A zero time removes them.
func (c *Conn) SetDeadline(t time.Time) error {
err := c.SetReadDeadline(t)
if err != nil {
return err
}
return c.SetWriteDeadline(t)
}
// SetReadDeadline makes reads after t fail with a timeout error. A zero time
// lets reads block indefinitely.
func (c *Conn) SetReadDeadline(t time.Time) error {
c.readDeadline = t
return c.SetReadTimeout(untilDeadline(t))
}
// SetWriteDeadline makes writes after t fail with a timeout error. A zero time
// lets writes block indefinitely.
func (c *Conn) SetWriteDeadline(t time.Time) error {
c.writeDeadline = t
return c.SetWriteTimeout(untilDeadline(t))
}
// SetReadTimeout limits how long a single Read waits for data before failing
// with a timeout error. Zero blocks indefinitely. This is what stops a peer
// that opens a connection and then stalls from holding a server worker: the
// connection, not the HTTP handler, owns the idle policy.
func (c *Conn) SetReadTimeout(timeout time.Duration) error {
return setSockTimeout(c.fd, syscall.SO_RCVTIMEO, timeout)
}
// SetWriteTimeout limits how long a single Write waits for the send buffer to
// drain before failing with a timeout error. Zero blocks indefinitely.
func (c *Conn) SetWriteTimeout(timeout time.Duration) error {
return setSockTimeout(c.fd, syscall.SO_SNDTIMEO, timeout)
}
// opError wraps a socket error the way the net package does, so that callers
// which test for a timeout with [net.Error] see one. A deadline that has passed
// surfaces from the kernel as EAGAIN, which on a blocking socket only ever
// means the timeout fired.
func (c *Conn) opError(op string, err error) error {
if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK {
err = errTimeout{}
}
return &net.OpError{Op: op, Net: "tcp", Source: c.local, Addr: c.remote, Err: err}
}
// errTimeout is the error a read or write past its deadline fails with.
type errTimeout struct{}
func (errTimeout) Error() string { return "i/o timeout" }
func (errTimeout) Timeout() bool { return true }
func (errTimeout) Temporary() bool { return true }
func untilDeadline(t time.Time) time.Duration {
if t.IsZero() {
return 0 // No deadline: block indefinitely.
}
remaining := time.Until(t)
if remaining <= 0 {
return time.Nanosecond // Already past: fail the next call, do not block.
}
return remaining
}
func setSockTimeout(fd, option int, timeout time.Duration) error {
tv := syscall.NsecToTimeval(int64(timeout))
return syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, option, &tv)
}
// Listener is a listening TCP socket bound to a local port. It implements
// [net.Listener].
type Listener struct {
fd int
local Addr
}
// Listen creates a listening TCP socket bound to port on all interfaces. A
// zero port lets the kernel choose one, see [Listener.Addr].
func (l *Listener) Listen(port uint16) error {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
if err != nil {
return err
}
// Allow quick rebind after restart.
if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
syscall.Close(fd)
return err
}
addr := &syscall.SockaddrInet4{Port: int(port)}
if err = syscall.Bind(fd, addr); err != nil {
syscall.Close(fd)
return err
}
if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
syscall.Close(fd)
return err
}
l.fd = fd
l.local = Addr{}
bound, err := syscall.Getsockname(fd)
if err != nil {
syscall.Close(fd)
return err
}
if sa4, ok := bound.(*syscall.SockaddrInet4); ok {
l.local = Addr(netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), uint16(sa4.Port)))
}
return nil
}
// Accept blocks until an incoming connection arrives and returns it. It
// allocates the connection, as [net.Listener] requires. A server that keeps its
// own connection storage should call [Listener.AcceptConn] instead.
func (l *Listener) Accept() (net.Conn, error) {
conn := new(Conn)
err := l.AcceptConn(conn)
if err != nil {
return nil, err
}
return conn, nil
}
// AcceptConn blocks until an incoming connection arrives and stores it in conn,
// reusing whatever conn already held. Nothing is allocated.
//
// The syscall is made by hand because [syscall.Accept] allocates the
// [syscall.Sockaddr] it returns, one per accepted connection: the kernel is
// given address storage this call owns instead, and the address is read out of
// it into conn.
func (l *Listener) AcceptConn(conn *Conn) error {
var rsa syscall.RawSockaddrAny
salen := uint32(unsafe.Sizeof(rsa))
nfd, _, errno := syscall.Syscall6(sysaccept, uintptr(l.fd),
uintptr(unsafe.Pointer(&rsa)), uintptr(unsafe.Pointer(&salen)), 0, 0, 0)
if errno != 0 {
return errno
}
*conn = Conn{fd: int(nfd), local: l.local}
if rsa.Addr.Family == syscall.AF_INET {
sa4 := (*syscall.RawSockaddrInet4)(unsafe.Pointer(&rsa))
// Port is in network byte order in the sockaddr the kernel filled.
port := uint16(sa4.Port<<8) | uint16(sa4.Port>>8)
conn.remote = Addr(netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), port))
}
return nil
}
// Addr returns the address the socket is bound to, which carries the port the
// kernel picked when the listener was created with port zero.
func (l *Listener) Addr() net.Addr { return l.local }
// Port returns the port the socket is bound to.
func (l *Listener) Port() uint16 { return l.local.AddrPort().Port() }
// Close closes the listening socket.
func (l *Listener) Close() error { return syscall.Close(l.fd) }
+214
View File
@@ -0,0 +1,214 @@
package tls
import (
"encoding/binary"
"unsafe"
)
// maxBuilderNest is the deepest length-prefix nesting the [Builder] supports.
// The deepest real TLS 1.3 structure is the ClientHello key_share extension:
// handshake body, extensions block, extension, client_shares list,
// key_exchange. Eight leaves generous headroom.
const maxBuilderNest = 8
// Builder writes TLS wire structures into a caller-supplied buffer that never
// grows. It exists because TLS nests length-prefixed vectors several levels
// deep and the length of each is only known after its contents are written;
// the standard answer, golang.org/x/crypto/cryptobyte, allocates.
//
// Open a length prefix, write the contents, then Close to backpatch the real
// length:
//
// var b tls.Builder
// b.Reset(buf)
// b.AddU16(uint16(tls.ExtSupportedVersions))
// b.OpenU16() // extension_data length
// b.AddU16(tls.VersionTLS13)
// b.Close()
// out, err := b.Bytes()
//
// Errors are sticky: once the destination buffer overflows or the nesting is
// misused, every later call is a no-op and [Builder.Err] reports the first
// failure. Callers therefore need to check only once, at the end.
//
// The zero Builder is not usable; call [Builder.Reset] first.
type Builder struct {
buf []byte
stack [maxBuilderNest]int32 // offset of each open length field
width [maxBuilderNest]int8 // 1, 2 or 3 byte length prefix
n int8 // open prefix count
err error
}
// Reset prepares the Builder to write into dst, discarding any previous state
// and error. The Builder writes into dst[:0] and never reallocates, so dst's
// capacity is a hard ceiling on the structure being built.
func (b *Builder) Reset(dst []byte) {
b.buf = dst[:0]
b.n = 0
b.err = nil
}
// Err returns the first error encountered since [Builder.Reset], if any.
func (b *Builder) Err() error { return b.err }
// Len returns the number of bytes written so far, including the placeholder
// bytes of any currently open length prefix.
func (b *Builder) Len() int { return len(b.buf) }
// Bytes returns the built structure. It reports an error if the Builder failed
// at any point, or if a length prefix was opened and never closed, since the
// resulting bytes would contain an unpatched placeholder.
func (b *Builder) Bytes() ([]byte, error) {
if b.err != nil {
return nil, b.err
}
if b.n != 0 {
return nil, errBuilderUnbal
}
return b.buf, nil
}
// fail records err if no error has been recorded yet.
func (b *Builder) fail(err error) {
if b.err == nil {
b.err = err
}
}
// grow extends the buffer by n bytes and returns the new region, or nil if the
// buffer is full or the Builder has already failed.
func (b *Builder) grow(n int) []byte {
if b.err != nil {
return nil
}
if n > cap(b.buf)-len(b.buf) {
b.fail(errShortBuffer)
return nil
}
start := len(b.buf)
b.buf = b.buf[:start+n]
return b.buf[start:]
}
// AddU8 appends a single byte.
func (b *Builder) AddU8(v uint8) {
if p := b.grow(1); p != nil {
p[0] = v
}
}
// AddU16 appends a big-endian uint16.
func (b *Builder) AddU16(v uint16) {
if p := b.grow(2); p != nil {
binary.BigEndian.PutUint16(p, v)
}
}
// AddU24 appends a big-endian 24-bit value, the length encoding used by
// handshake message headers and certificate entries.
func (b *Builder) AddU24(v uint32) {
if p := b.grow(3); p != nil {
p[0] = byte(v >> 16)
p[1] = byte(v >> 8)
p[2] = byte(v)
}
}
// AddU32 appends a big-endian uint32.
func (b *Builder) AddU32(v uint32) {
if p := b.grow(4); p != nil {
binary.BigEndian.PutUint32(p, v)
}
}
// AddBytes appends raw bytes.
func (b *Builder) AddBytes(v []byte) {
if p := b.grow(len(v)); p != nil {
copy(p, v)
}
}
// AddString appends the bytes of s without allocating. TLS labels and ALPN
// protocol names are naturally string constants, and converting them with
// []byte(s) would copy to the heap on every call.
func (b *Builder) AddString(s string) {
if len(s) == 0 {
return
}
// unsafe.Slice over the string's backing array; the bytes are only read,
// and only for the duration of the copy inside AddBytes.
b.AddBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
}
// openN reserves width bytes for a length prefix whose value is filled in by
// the matching [Builder.Close].
func (b *Builder) openN(width int8) {
if b.err != nil {
return
}
if int(b.n) >= maxBuilderNest {
b.fail(errBuilderNest)
return
}
start := len(b.buf)
if b.grow(int(width)) == nil {
return
}
b.stack[b.n] = int32(start)
b.width[b.n] = width
b.n++
}
// OpenU8 begins a vector with a one-byte length prefix.
func (b *Builder) OpenU8() { b.openN(1) }
// OpenU16 begins a vector with a two-byte length prefix.
func (b *Builder) OpenU16() { b.openN(2) }
// OpenU24 begins a vector with a three-byte length prefix, as used by the
// handshake message header.
func (b *Builder) OpenU24() { b.openN(3) }
// Close ends the innermost open vector and backpatches its length prefix with
// the number of bytes written since the matching Open.
//
// A length that does not fit the reserved prefix width is a failure rather
// than a silent truncation: writing a value modulo 2^16 into a two-byte prefix
// would produce a structurally valid but semantically wrong record.
func (b *Builder) Close() {
if b.err != nil {
return
}
if b.n == 0 {
b.fail(errBuilderUnbal)
return
}
b.n--
start := int(b.stack[b.n])
width := int(b.width[b.n])
n := len(b.buf) - start - width
p := b.buf[start:]
switch width {
case 1:
if n > 0xff {
b.fail(errBadLength)
return
}
p[0] = byte(n)
case 2:
if n > 0xffff {
b.fail(errBadLength)
return
}
binary.BigEndian.PutUint16(p, uint16(n))
case 3:
if n > 0xffffff {
b.fail(errBadLength)
return
}
p[0] = byte(n >> 16)
p[1] = byte(n >> 8)
p[2] = byte(n)
}
}
+242
View File
@@ -0,0 +1,242 @@
package tls_test
import (
"errors"
"testing"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/tls"
)
// buildSupportedVersions writes the extension a TLS 1.3 ServerHello carries,
// exercising one level of nesting.
func buildSupportedVersions(b *tls.Builder) {
b.AddU16(uint16(tls.ExtSupportedVersions))
b.OpenU16()
b.AddU16(tls.VersionTLS13)
b.Close()
}
func TestBuilderRoundTripThroughParser(t *testing.T) {
// Build an extensions block, then walk it back with the parser. Agreement
// between the two is the property that matters.
var b tls.Builder
buf := make([]byte, 64)
b.Reset(buf)
b.OpenU16() // extensions block length
buildSupportedVersions(&b)
b.AddU16(uint16(tls.ExtKeyShare))
b.OpenU16()
b.OpenU16() // client_shares
b.AddU16(uint16(tls.GroupX25519))
b.OpenU16()
b.AddBytes(make([]byte, 8))
b.Close()
b.Close()
b.Close()
b.Close()
out, err := b.Bytes()
if err != nil {
t.Fatal(err)
}
// Strip the outer block length the way ClientHelloFrame.Extensions does.
if len(out) < 2 {
t.Fatal("output too short")
}
exts := out[2:]
if int(out[0])<<8|int(out[1]) != len(exts) {
t.Fatalf("outer length %d != %d", int(out[0])<<8|int(out[1]), len(exts))
}
var seen []tls.ExtensionType
err = tls.ForEachExtension(exts, func(ext tls.ExtensionType, data []byte) error {
seen = append(seen, ext)
if ext == tls.ExtKeyShare {
return tls.ForEachKeyShare(data, func(g tls.NamedGroup, key []byte) error {
if g != tls.GroupX25519 || len(key) != 8 {
t.Errorf("key share got %v len %d", g, len(key))
}
return nil
})
}
return nil
})
if err != nil {
t.Fatal(err)
}
if len(seen) != 2 || seen[0] != tls.ExtSupportedVersions || seen[1] != tls.ExtKeyShare {
t.Errorf("walked %v", seen)
}
}
func TestBuilderStickyShortBuffer(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 4))
b.AddU32(0x01020304) // exactly fills
if b.Err() != nil {
t.Fatalf("unexpected error filling buffer: %v", b.Err())
}
b.AddU8(0xff) // overflows
if !errors.Is(b.Err(), lneto.ErrShortBuffer) {
t.Fatalf("got %v want ErrShortBuffer", b.Err())
}
// Once failed, later writes must not panic, must not write, and must not
// replace the original error.
b.AddBytes(make([]byte, 100))
b.OpenU16()
b.Close()
if !errors.Is(b.Err(), lneto.ErrShortBuffer) {
t.Fatalf("error changed to %v", b.Err())
}
if _, err := b.Bytes(); !errors.Is(err, lneto.ErrShortBuffer) {
t.Fatalf("Bytes returned %v", err)
}
}
func TestBuilderUnclosedPrefixIsAnError(t *testing.T) {
// Returning bytes with an unpatched placeholder would emit a structurally
// wrong record that looks valid.
var b tls.Builder
b.Reset(make([]byte, 16))
b.OpenU16()
b.AddU8(1)
if _, err := b.Bytes(); err == nil {
t.Error("Bytes accepted an unclosed length prefix")
}
}
func TestBuilderUnbalancedClose(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 16))
b.Close()
if b.Err() == nil {
t.Error("Close without Open accepted")
}
}
func TestBuilderNestingLimit(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 64))
for range 8 {
b.OpenU8()
}
if b.Err() != nil {
t.Fatalf("8 levels rejected: %v", b.Err())
}
b.OpenU8()
if b.Err() == nil {
t.Error("9th nesting level accepted")
}
}
func TestBuilderLengthOverflowRejected(t *testing.T) {
// 300 bytes cannot be described by a one-byte prefix. Truncating modulo 256
// would produce a valid-looking but wrong structure, so this must fail.
var b tls.Builder
b.Reset(make([]byte, 512))
b.OpenU8()
b.AddBytes(make([]byte, 300))
b.Close()
if !errors.Is(b.Err(), lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", b.Err())
}
}
func TestBuilderU24RoundTrip(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 8))
b.AddU8(byte(tls.HandshakeTypeServerHello))
b.OpenU24()
b.AddBytes([]byte{1, 2, 3})
b.Close()
out, err := b.Bytes()
if err != nil {
t.Fatal(err)
}
hf, err := tls.NewHandshakeFrame(out)
if err != nil {
t.Fatal(err)
}
if hf.MsgType() != tls.HandshakeTypeServerHello {
t.Errorf("type got %v", hf.MsgType())
}
if hf.Length() != 3 {
t.Errorf("length got %d want 3", hf.Length())
}
if !hf.Complete() || len(hf.Body()) != 3 {
t.Errorf("body got % x", hf.Body())
}
}
func TestBuilderAddStringMatchesAddBytes(t *testing.T) {
const label = "tls13 derived"
var b1, b2 tls.Builder
buf1, buf2 := make([]byte, 32), make([]byte, 32)
b1.Reset(buf1)
b1.AddString(label)
b2.Reset(buf2)
b2.AddBytes([]byte(label))
out1, err1 := b1.Bytes()
out2, err2 := b2.Bytes()
if err1 != nil || err2 != nil {
t.Fatalf("%v %v", err1, err2)
}
if string(out1) != string(out2) {
t.Errorf("AddString %q != AddBytes %q", out1, out2)
}
}
func TestBuilderZeroAlloc(t *testing.T) {
// The whole point of not using cryptobyte. A regression here means the
// outbound handshake path started allocating per connection.
buf := make([]byte, 256)
var b tls.Builder
n := testing.AllocsPerRun(100, func() {
b.Reset(buf)
b.AddU8(byte(tls.HandshakeTypeServerHello))
b.OpenU24()
b.AddU16(tls.VersionTLS12)
b.AddBytes(make([]byte, 0, 0)) // no-op, must not allocate
b.OpenU16()
buildSupportedVersions(&b)
b.Close()
b.AddString("x")
b.Close()
_, _ = b.Bytes()
})
if n != 0 {
t.Errorf("Builder allocated %v times per run, want 0", n)
}
}
func FuzzBuilderNeverEscapesBuffer(f *testing.F) {
f.Add([]byte{1, 2, 3}, uint8(16))
f.Fuzz(func(t *testing.T, payload []byte, size uint8) {
buf := make([]byte, size)
canary := make([]byte, len(buf))
var b tls.Builder
b.Reset(buf)
b.OpenU16()
b.AddBytes(payload)
b.OpenU8()
b.AddBytes(payload)
b.Close()
b.Close()
out, err := b.Bytes()
if err != nil {
return
}
if len(out) > len(buf) {
t.Fatalf("wrote %d bytes into a %d byte buffer", len(out), len(buf))
}
// Whatever was produced must parse back as a well-formed vector.
if len(out) < 2 {
t.Fatalf("output %d bytes is too short to hold its own prefix", len(out))
}
if declared := int(out[0])<<8 | int(out[1]); declared != len(out)-2 {
t.Fatalf("declared %d != actual %d", declared, len(out)-2)
}
_ = canary
})
}
+275
View File
@@ -0,0 +1,275 @@
// Package tls implements a heapless, preallocated TLS 1.3 server (RFC 8446).
//
// All buffers are supplied by the caller and never grow. There is no
// allocation in the steady-state record datapath, no goroutines, and no
// dependency on an operating system. A [Conn] wraps any [io.ReadWriter], so it
// works over a lneto tcp.Conn, over net.Pipe, or over a bytes.Buffer in tests.
//
// # Scope
//
// TLS 1.3 only. There is deliberately no TLS 1.2 fallback, no renegotiation,
// no 0-RTT/early data, and no client certificate support. Each omission
// removes a whole class of attack surface.
//
// # Cryptographic primitives
//
// This package implements the record layer, the key schedule and the handshake
// state machine, but ships no cryptographic primitives of its own. Concrete
// hashes, AEADs, key agreements and signers cross the [Hasher], [AEADSuite],
// [KeyAgreement] and [Signer] interfaces. Standard library implementations
// live in the tlsstd subpackage; a hardware AEAD or a secure-element signer
// slots in at the same seam without protocol code changing.
//
// Note that on embedded targets the Signer is expected to be supplied by the
// integrator: Go's crypto/ecdsa carries an 88kB precomputed P-256 basepoint
// table that lands in RAM, which is a third of an RP2040's SRAM.
//
// # Randomness
//
// Config.Rand is required and has no default. Embedded targets frequently have
// a weak or stubbed entropy source, and a biased ECDSA nonce recovers the
// signing key from two signatures, so the choice is made explicit rather than
// silently defaulted. Never seed anything here from internal.Prand32/64; those
// are for TCP initial sequence numbers and DNS IDs, not for key material.
package tls
import (
"errors"
"github.com/soypat/lneto"
)
// Sizes of fixed length headers and the record size limits of RFC 8446 5.2.
const (
// SizeHeaderRecord is the size of a TLSPlaintext/TLSCiphertext header:
// content type(1) + legacy record version(2) + length(2).
SizeHeaderRecord = 5
// SizeHeaderHandshake is the size of a Handshake header:
// message type(1) + 24-bit length(3).
SizeHeaderHandshake = 4
// MaxPlaintext is the largest legal TLSPlaintext.fragment.
MaxPlaintext = 1 << 14 // 16384
// MaxCiphertext is the largest legal TLSCiphertext.encrypted_record.
// The extra 256 bytes cover the content type byte, padding and the AEAD tag.
MaxCiphertext = 1<<14 + 256 // 16640
// MaxRecord is the largest legal record as it appears on the wire.
MaxRecord = SizeHeaderRecord + MaxCiphertext // 16645
// MinRecordSizeLimit is the smallest value a peer may advertise in the
// record_size_limit extension of RFC 8449 4.
MinRecordSizeLimit = 64
)
//go:generate stringer -type=ContentType,HandshakeType,ExtensionType,AlertDescription,AlertLevel,NamedGroup,SignatureScheme,CipherSuite -linecomment -output stringers.go .
// ContentType is the outermost record demultiplexing tag of RFC 8446 5.1.
type ContentType uint8
// Record content types. Values below are the only ones TLS 1.3 defines;
// everything else must be rejected with an unexpected_message alert.
const (
ContentTypeInvalid ContentType = 0 // invalid
ContentTypeChangeCipherSpec ContentType = 20 // change_cipher_spec
ContentTypeAlert ContentType = 21 // alert
ContentTypeHandshake ContentType = 22 // handshake
ContentTypeApplicationData ContentType = 23 // application_data
)
// HandshakeType identifies a handshake message, RFC 8446 4.
type HandshakeType uint8
// Handshake message types. Types this server never sends or accepts are still
// listed so that a peer sending one can be logged and rejected precisely.
const (
HandshakeTypeClientHello HandshakeType = 1 // client_hello
HandshakeTypeServerHello HandshakeType = 2 // server_hello
HandshakeTypeNewSessionTicket HandshakeType = 4 // new_session_ticket
HandshakeTypeEndOfEarlyData HandshakeType = 5 // end_of_early_data
HandshakeTypeEncryptedExtensions HandshakeType = 8 // encrypted_extensions
HandshakeTypeCertificate HandshakeType = 11 // certificate
HandshakeTypeCertificateRequest HandshakeType = 13 // certificate_request
HandshakeTypeCertificateVerify HandshakeType = 15 // certificate_verify
HandshakeTypeFinished HandshakeType = 20 // finished
HandshakeTypeKeyUpdate HandshakeType = 24 // key_update
HandshakeTypeMessageHash HandshakeType = 254 // message_hash
)
// ExtensionType identifies a hello extension, RFC 8446 4.2.
type ExtensionType uint16
// Extension types. Those marked "ignored" are ones a browser sends and this
// server parses past without erroring; being strict about unknown extensions
// breaks real clients.
const (
ExtServerName ExtensionType = 0 // server_name
ExtMaxFragmentLength ExtensionType = 1 // max_fragment_length
ExtStatusRequest ExtensionType = 5 // status_request
ExtSupportedGroups ExtensionType = 10 // supported_groups
ExtSignatureAlgorithms ExtensionType = 13 // signature_algorithms
ExtALPN ExtensionType = 16 // application_layer_protocol_negotiation
ExtSignedCertificateTimestamp ExtensionType = 18 // signed_certificate_timestamp
ExtPadding ExtensionType = 21 // padding
ExtExtendedMasterSecret ExtensionType = 23 // extended_master_secret
ExtCompressCertificate ExtensionType = 27 // compress_certificate
ExtRecordSizeLimit ExtensionType = 28 // record_size_limit
ExtSessionTicket ExtensionType = 35 // session_ticket
ExtPreSharedKey ExtensionType = 41 // pre_shared_key
ExtEarlyData ExtensionType = 42 // early_data
ExtSupportedVersions ExtensionType = 43 // supported_versions
ExtCookie ExtensionType = 44 // cookie
ExtPSKKeyExchangeModes ExtensionType = 45 // psk_key_exchange_modes
ExtCertificateAuthorities ExtensionType = 47 // certificate_authorities
ExtSignatureAlgorithmsCert ExtensionType = 50 // signature_algorithms_cert
ExtKeyShare ExtensionType = 51 // key_share
ExtApplicationSettings ExtensionType = 17513 // application_settings
ExtEncryptedClientHello ExtensionType = 65037 // encrypted_client_hello
ExtRenegotiationInfo ExtensionType = 65281 // renegotiation_info
)
// AlertLevel is the legacy severity byte of an alert. In TLS 1.3 every alert
// except close_notify and user_canceled is fatal regardless of this field
// (RFC 8446 6.1), so it is carried for wire compatibility only and must never
// be used to decide whether to continue.
type AlertLevel uint8
// Alert levels.
const (
AlertLevelWarning AlertLevel = 1 // warning
AlertLevelFatal AlertLevel = 2 // fatal
)
// AlertDescription is the alert code of RFC 8446 6.
type AlertDescription uint8
// Alert descriptions. Only the subset a TLS 1.3 server can legitimately send
// or receive is listed.
const (
AlertCloseNotify AlertDescription = 0 // close_notify
AlertUnexpectedMessage AlertDescription = 10 // unexpected_message
AlertBadRecordMAC AlertDescription = 20 // bad_record_mac
AlertRecordOverflow AlertDescription = 22 // record_overflow
AlertHandshakeFailure AlertDescription = 40 // handshake_failure
AlertBadCertificate AlertDescription = 42 // bad_certificate
AlertUnsupportedCertificate AlertDescription = 43 // unsupported_certificate
AlertCertificateRevoked AlertDescription = 44 // certificate_revoked
AlertCertificateExpired AlertDescription = 45 // certificate_expired
AlertCertificateUnknown AlertDescription = 46 // certificate_unknown
AlertIllegalParameter AlertDescription = 47 // illegal_parameter
AlertUnknownCA AlertDescription = 48 // unknown_ca
AlertAccessDenied AlertDescription = 49 // access_denied
AlertDecodeError AlertDescription = 50 // decode_error
AlertDecryptError AlertDescription = 51 // decrypt_error
AlertProtocolVersion AlertDescription = 70 // protocol_version
AlertInsufficientSecurity AlertDescription = 71 // insufficient_security
AlertInternalError AlertDescription = 80 // internal_error
AlertInappropriateFallback AlertDescription = 86 // inappropriate_fallback
AlertUserCanceled AlertDescription = 90 // user_canceled
AlertMissingExtension AlertDescription = 109 // missing_extension
AlertUnsupportedExtension AlertDescription = 110 // unsupported_extension
AlertUnrecognizedName AlertDescription = 112 // unrecognized_name
AlertBadCertificateStatusResponse AlertDescription = 113 // bad_certificate_status_response
AlertUnknownPSKIdentity AlertDescription = 115 // unknown_psk_identity
AlertCertificateRequired AlertDescription = 116 // certificate_required
AlertNoApplicationProtocol AlertDescription = 120 // no_application_protocol
)
// Protocol versions as they appear on the wire.
const (
// VersionTLS12 is the value TLS 1.3 requires in ClientHello.legacy_version
// and in ServerHello.legacy_version for middlebox compatibility.
VersionTLS12 uint16 = 0x0303
// VersionTLS13 is the real negotiated version, carried only in the
// supported_versions extension.
VersionTLS13 uint16 = 0x0304
// VersionTLS10 appears in the legacy_record_version of an initial
// ClientHello record. The field is ignored entirely on receipt.
VersionTLS10 uint16 = 0x0301
)
// NamedGroup identifies a key exchange group, RFC 8446 4.2.7.
type NamedGroup uint16
// Named groups. Only X25519 is implemented; the rest are recognized so that
// group selection and HelloRetryRequest can report precisely what was offered.
const (
GroupSECP256R1 NamedGroup = 0x0017 // secp256r1
GroupSECP384R1 NamedGroup = 0x0018 // secp384r1
GroupSECP521R1 NamedGroup = 0x0019 // secp521r1
GroupX25519 NamedGroup = 0x001d // x25519
GroupX448 NamedGroup = 0x001e // x448
GroupX25519MLKEM768 NamedGroup = 0x11ec // x25519mlkem768
)
// SignatureScheme identifies a signature algorithm, RFC 8446 4.2.3.
type SignatureScheme uint16
// Signature schemes. Browsers do not accept Ed25519 certificates, so
// [SigECDSAP256SHA256] is the practical minimum for a public-facing server.
const (
SigRSAPKCS1SHA256 SignatureScheme = 0x0401 // rsa_pkcs1_sha256
SigRSAPKCS1SHA384 SignatureScheme = 0x0501 // rsa_pkcs1_sha384
SigRSAPKCS1SHA512 SignatureScheme = 0x0601 // rsa_pkcs1_sha512
SigECDSAP256SHA256 SignatureScheme = 0x0403 // ecdsa_secp256r1_sha256
SigECDSAP384SHA384 SignatureScheme = 0x0503 // ecdsa_secp384r1_sha384
SigECDSAP521SHA512 SignatureScheme = 0x0603 // ecdsa_secp521r1_sha512
SigRSAPSSRSAESHA256 SignatureScheme = 0x0804 // rsa_pss_rsae_sha256
SigRSAPSSRSAESHA384 SignatureScheme = 0x0805 // rsa_pss_rsae_sha384
SigRSAPSSRSAESHA512 SignatureScheme = 0x0806 // rsa_pss_rsae_sha512
SigEd25519 SignatureScheme = 0x0807 // ed25519
SigRSAPSSPSSSHA256 SignatureScheme = 0x0809 // rsa_pss_pss_sha256
)
// CipherSuite identifies an AEAD plus hash pair, RFC 8446 B.4.
type CipherSuite uint16
// TLS 1.3 cipher suites. Only [SuiteAES128GCMSHA256] is implemented; it is the
// one suite RFC 8446 9.1 makes mandatory to implement.
const (
SuiteAES128GCMSHA256 CipherSuite = 0x1301 // TLS_AES_128_GCM_SHA256
SuiteAES256GCMSHA384 CipherSuite = 0x1302 // TLS_AES_256_GCM_SHA384
SuiteChaCha20Poly1305SHA256 CipherSuite = 0x1303 // TLS_CHACHA20_POLY1305_SHA256
SuiteAES128CCMSHA256 CipherSuite = 0x1304 // TLS_AES_128_CCM_SHA256
SuiteAES128CCM8SHA256 CipherSuite = 0x1305 // TLS_AES_128_CCM_8_SHA256
)
// IsGREASE reports whether v is one of the 16 reserved GREASE values of
// RFC 8701. Chrome injects GREASE values into its offered cipher suites,
// supported groups, extensions, ALPN protocol list and key shares. They carry
// no meaning and must be skipped wherever they appear; rejecting them breaks
// Chrome outright.
//
// GREASE values have both bytes equal and of the form 0x?a.
func IsGREASE(v uint16) bool {
return v&0x0f0f == 0x0a0a && v>>8 == v&0xff
}
// ErrNeedMore is returned by incremental parsers and by the record layer when
// the input available so far is a valid prefix but not yet a complete unit. It
// is not a failure: the caller should retry once more data has arrived.
//
// It is deliberately distinct from [lneto.ErrTruncatedFrame], which means the
// data is complete but malformed.
var ErrNeedMore = errors.New("tls: need more data")
// ErrUnexpectedClose is returned by Conn.Read when the underlying transport
// reached EOF before a close_notify alert was received. Returning io.EOF here
// instead would let an attacker who can inject a TCP FIN or RST silently
// truncate a response or a request body. Callers must treat this as a failure
// unless the application layer has independently confirmed the message was
// complete, for example by satisfying a Content-Length.
var ErrUnexpectedClose = errors.New("tls: connection closed without close_notify")
// Errors reported by frame constructors and walkers. These reuse the generic
// lneto error set so that callers can compare against a single vocabulary.
var (
errTruncated error = lneto.ErrTruncatedFrame
errShortBuffer error = lneto.ErrShortBuffer
errBadLength error = lneto.ErrInvalidLengthField
errBadField error = lneto.ErrInvalidField
errUnsupported error = lneto.ErrUnsupported
errTrailingBytes = errors.New("tls: trailing bytes after structure")
errBuilderNest = errors.New("tls: builder nesting depth exceeded")
errBuilderUnbal = errors.New("tls: builder length prefix unbalanced")
errAllZeroPlaintext = errors.New("tls: inner plaintext is all padding")
)
+485
View File
@@ -0,0 +1,485 @@
package tls
import (
"encoding/binary"
"github.com/soypat/lneto"
)
// RecordFrame provides zero-copy access to a TLS record (RFC 8446 5.1).
//
// struct {
// ContentType type; // 1 byte
// ProtocolVersion legacy_record_version; // 2 bytes
// uint16 length; // 2 bytes
// opaque fragment[length];
// } TLSPlaintext;
//
// A RecordFrame may be constructed over a buffer holding only the header, so
// that [RecordFrame.Length] can be consulted to decide how many more bytes to
// read. Accessors that reach into the fragment return nil until the whole
// record is present; call [RecordFrame.Complete] to test for that explicitly.
type RecordFrame struct {
buf []byte
}
// NewRecordFrame wraps buf as a [RecordFrame]. It validates only that the
// 5-byte header is present, since the fragment commonly arrives later.
func NewRecordFrame(buf []byte) (RecordFrame, error) {
if len(buf) < SizeHeaderRecord {
return RecordFrame{}, lneto.ErrTruncatedFrame
}
return RecordFrame{buf: buf}, nil
}
// ContentType returns the record's outer content type. For a protected record
// this is always [ContentTypeApplicationData]; the real type lives in the
// encrypted [InnerPlaintext].
func (rf RecordFrame) ContentType() ContentType { return ContentType(rf.buf[0]) }
// SetContentType sets the outer content type.
func (rf RecordFrame) SetContentType(ct ContentType) { rf.buf[0] = byte(ct) }
// LegacyVersion returns the legacy_record_version field. TLS 1.3 requires
// receivers to ignore this field entirely; it is exposed for logging only.
func (rf RecordFrame) LegacyVersion() uint16 {
return binary.BigEndian.Uint16(rf.buf[1:3])
}
// SetLegacyVersion sets the legacy_record_version field.
func (rf RecordFrame) SetLegacyVersion(v uint16) {
binary.BigEndian.PutUint16(rf.buf[1:3], v)
}
// Length returns the declared fragment length. It is attacker controlled and
// must be checked against [MaxCiphertext] before being used to size a read;
// [RecordFrame.ValidateSize] does this.
func (rf RecordFrame) Length() uint16 {
return binary.BigEndian.Uint16(rf.buf[3:5])
}
// SetLength sets the declared fragment length.
func (rf RecordFrame) SetLength(n uint16) {
binary.BigEndian.PutUint16(rf.buf[3:5], n)
}
// RecordLength returns the total wire size of this record, header included.
func (rf RecordFrame) RecordLength() int {
return SizeHeaderRecord + int(rf.Length())
}
// Complete reports whether the whole record, header and fragment, is present
// in the underlying buffer.
func (rf RecordFrame) Complete() bool {
return len(rf.buf) >= rf.RecordLength()
}
// Payload returns the record fragment, or nil if the whole record has not
// arrived yet. The result aliases the underlying buffer.
func (rf RecordFrame) Payload() []byte {
if !rf.Complete() {
return nil
}
return rf.buf[SizeHeaderRecord:rf.RecordLength()]
}
// RawData returns the record bytes, header included, truncated to the declared
// length when the full record is present.
func (rf RecordFrame) RawData() []byte {
if !rf.Complete() {
return rf.buf
}
return rf.buf[:rf.RecordLength()]
}
// ValidateSize adds an error to v if the record is structurally invalid.
// It does not require the fragment to have arrived; it only rejects a declared
// length that could never be legal.
func (rf RecordFrame) ValidateSize(v *lneto.Validator) {
if len(rf.buf) < SizeHeaderRecord {
v.AddError(lneto.ErrTruncatedFrame)
return
}
if rf.Length() > MaxCiphertext {
// Checked before the length is ever used to size a read.
v.AddError(lneto.ErrInvalidLengthField)
}
}
// InnerPlaintext provides access to a decrypted TLSInnerPlaintext
// (RFC 8446 5.2):
//
// struct {
// opaque content[length];
// ContentType type;
// uint8 zeros[length_of_padding];
// } TLSInnerPlaintext;
//
// The real content type is the last non-zero byte, and everything after the
// content is padding that must be stripped before use.
type InnerPlaintext struct {
buf []byte // content only, padding and type byte already stripped
ctype ContentType
padding int
}
// NewInnerPlaintext scans decrypted for its trailing content type byte,
// stripping any zero padding that follows it.
//
// A record whose plaintext is entirely zeros carries no content type and is a
// protocol violation; it is reported so the caller can send an
// unexpected_message alert rather than silently treating it as empty.
func NewInnerPlaintext(decrypted []byte) (InnerPlaintext, error) {
i := len(decrypted) - 1
for i >= 0 && decrypted[i] == 0 {
i--
}
if i < 0 {
return InnerPlaintext{}, errAllZeroPlaintext
}
return InnerPlaintext{
buf: decrypted[:i],
ctype: ContentType(decrypted[i]),
padding: len(decrypted) - i - 1,
}, nil
}
// ContentType returns the true content type recovered from the inner plaintext.
func (ip InnerPlaintext) ContentType() ContentType { return ip.ctype }
// Content returns the plaintext with the content type byte and padding removed.
// The result aliases the buffer passed to [NewInnerPlaintext].
func (ip InnerPlaintext) Content() []byte { return ip.buf }
// PaddingLen returns how many padding bytes followed the content type byte.
func (ip InnerPlaintext) PaddingLen() int { return ip.padding }
// HandshakeFrame provides zero-copy access to a handshake message
// (RFC 8446 4):
//
// struct {
// HandshakeType msg_type; // 1 byte
// uint24 length; // 3 bytes
// opaque body[length];
// } Handshake;
//
// As with [RecordFrame], a HandshakeFrame may be constructed over a buffer
// holding only the 4-byte header so that the body length can be consulted
// before the rest has arrived.
type HandshakeFrame struct {
buf []byte
}
// NewHandshakeFrame wraps buf as a [HandshakeFrame], validating that the
// 4-byte header is present.
func NewHandshakeFrame(buf []byte) (HandshakeFrame, error) {
if len(buf) < SizeHeaderHandshake {
return HandshakeFrame{}, lneto.ErrTruncatedFrame
}
return HandshakeFrame{buf: buf}, nil
}
// MsgType returns the handshake message type.
func (hf HandshakeFrame) MsgType() HandshakeType { return HandshakeType(hf.buf[0]) }
// SetMsgType sets the handshake message type.
func (hf HandshakeFrame) SetMsgType(t HandshakeType) { hf.buf[0] = byte(t) }
// Length returns the declared 24-bit body length. It is attacker controlled;
// the value is returned as an int32 rather than an int so that behaviour is
// identical on 32- and 64-bit targets.
func (hf HandshakeFrame) Length() int32 {
return int32(hf.buf[1])<<16 | int32(hf.buf[2])<<8 | int32(hf.buf[3])
}
// SetLength sets the declared 24-bit body length. Values outside the 24-bit
// range are silently masked; callers building messages should ensure the body
// fits first.
func (hf HandshakeFrame) SetLength(n int32) {
hf.buf[1] = byte(n >> 16)
hf.buf[2] = byte(n >> 8)
hf.buf[3] = byte(n)
}
// MessageLength returns the total size of this handshake message, header
// included.
func (hf HandshakeFrame) MessageLength() int {
return SizeHeaderHandshake + int(hf.Length())
}
// Complete reports whether the entire handshake message is present.
func (hf HandshakeFrame) Complete() bool {
return len(hf.buf) >= hf.MessageLength()
}
// Body returns the handshake message body, or nil if the whole message has not
// arrived. The result aliases the underlying buffer.
func (hf HandshakeFrame) Body() []byte {
if !hf.Complete() {
return nil
}
return hf.buf[SizeHeaderHandshake:hf.MessageLength()]
}
// RawData returns the handshake message bytes, header included. This is what
// must be fed to the transcript hash: the header is hashed along with the body,
// exactly once per message, even when the message spanned several records.
func (hf HandshakeFrame) RawData() []byte {
if !hf.Complete() {
return hf.buf
}
return hf.buf[:hf.MessageLength()]
}
// ValidateSize adds an error to v if the handshake header is malformed.
func (hf HandshakeFrame) ValidateSize(v *lneto.Validator) {
if len(hf.buf) < SizeHeaderHandshake {
v.AddError(lneto.ErrTruncatedFrame)
return
}
if hf.Length() > MaxPlaintext {
// No handshake message this server accepts approaches 2^14 bytes.
v.AddError(lneto.ErrInvalidLengthField)
}
}
// ExtensionFrame provides access to a single hello extension (RFC 8446 4.2):
//
// struct {
// ExtensionType extension_type; // 2 bytes
// opaque extension_data<0..2^16-1>;
// } Extension;
type ExtensionFrame struct {
buf []byte
}
// NewExtensionFrame wraps buf as an [ExtensionFrame]. Unlike the record and
// handshake frames, an extension is only ever parsed out of a fully buffered
// hello, so the whole extension must be present.
func NewExtensionFrame(buf []byte) (ExtensionFrame, error) {
if len(buf) < 4 {
return ExtensionFrame{}, lneto.ErrTruncatedFrame
}
n := int(binary.BigEndian.Uint16(buf[2:4]))
if 4+n > len(buf) {
return ExtensionFrame{}, lneto.ErrTruncatedFrame
}
return ExtensionFrame{buf: buf[:4+n]}, nil
}
// Type returns the extension type.
func (ef ExtensionFrame) Type() ExtensionType {
return ExtensionType(binary.BigEndian.Uint16(ef.buf[0:2]))
}
// Length returns the declared extension_data length.
func (ef ExtensionFrame) Length() uint16 {
return binary.BigEndian.Uint16(ef.buf[2:4])
}
// Data returns the extension_data bytes.
func (ef ExtensionFrame) Data() []byte { return ef.buf[4:] }
// RawData returns the extension bytes, type and length included.
func (ef ExtensionFrame) RawData() []byte { return ef.buf }
// ForEachExtension walks an extension list, calling fn for each extension in
// wire order. exts is the contents of the extensions block, with the outer
// two-byte list length already stripped; [ClientHelloFrame.Extensions] returns
// it in that form.
//
// The walker is deliberately permissive about extension types: unknown types,
// including every GREASE value Chrome injects, are passed to fn like any other.
// It is the caller's switch that skips them. The walker is strict about
// framing: a length field that overruns the list aborts with
// [lneto.ErrTruncatedFrame].
//
// Modelled on tcp.OptionCodec.ForEachOption.
func ForEachExtension(exts []byte, fn func(ExtensionType, []byte) error) error {
for off := 0; off < len(exts); {
ef, err := NewExtensionFrame(exts[off:])
if err != nil {
return err
}
err = fn(ef.Type(), ef.Data())
if err != nil {
return err
}
off += len(ef.RawData())
}
return nil
}
// ForEachU16 walks a bare list of big-endian uint16 values, such as the
// cipher_suites vector of a ClientHello. b must have even length.
func ForEachU16(b []byte, fn func(uint16) error) error {
if len(b)%2 != 0 {
return lneto.ErrInvalidLengthField
}
for off := 0; off < len(b); off += 2 {
err := fn(binary.BigEndian.Uint16(b[off : off+2]))
if err != nil {
return err
}
}
return nil
}
// ForEachSupportedGroup walks the contents of a supported_groups extension,
// whose payload is a uint16-length-prefixed list of [NamedGroup] values.
func ForEachSupportedGroup(extData []byte, fn func(NamedGroup) error) error {
body, err := vectorU16(extData)
if err != nil {
return err
}
return ForEachU16(body, func(v uint16) error { return fn(NamedGroup(v)) })
}
// ForEachSignatureScheme walks the contents of a signature_algorithms (or
// signature_algorithms_cert) extension.
func ForEachSignatureScheme(extData []byte, fn func(SignatureScheme) error) error {
body, err := vectorU16(extData)
if err != nil {
return err
}
return ForEachU16(body, func(v uint16) error { return fn(SignatureScheme(v)) })
}
// ForEachSupportedVersion walks the contents of a supported_versions extension
// as it appears in a ClientHello. Note the prefix here is a single byte, unlike
// every other list in the hello; the ServerHello form carries a bare uint16
// instead and is not parsed by this function.
func ForEachSupportedVersion(extData []byte, fn func(uint16) error) error {
body, err := vectorU8(extData)
if err != nil {
return err
}
return ForEachU16(body, fn)
}
// ForEachKeyShare walks the client_shares list of a key_share extension:
//
// struct {
// NamedGroup group;
// opaque key_exchange<1..2^16-1>;
// } KeyShareEntry;
//
// GREASE key shares carry a deliberately absurd body, commonly a single byte,
// and must not be treated as malformed. The walker therefore places no
// constraint on key_exchange length beyond it fitting inside the list; group
// selection is the caller's job.
func ForEachKeyShare(extData []byte, fn func(NamedGroup, []byte) error) error {
body, err := vectorU16(extData)
if err != nil {
return err
}
for off := 0; off < len(body); {
if len(body)-off < 4 {
return lneto.ErrTruncatedFrame
}
group := NamedGroup(binary.BigEndian.Uint16(body[off : off+2]))
n := int(binary.BigEndian.Uint16(body[off+2 : off+4]))
off += 4
if n > len(body)-off {
return lneto.ErrTruncatedFrame
}
err = fn(group, body[off:off+n])
if err != nil {
return err
}
off += n
}
return nil
}
// ForEachALPNProto walks the protocol name list of an ALPN extension. Each
// name is a single-byte-length-prefixed string. Chrome includes a GREASE entry
// here too, so callers must match against their own offer list rather than
// assuming the first entry is meaningful.
//
// A zero-length protocol name is a protocol violation and aborts the walk.
func ForEachALPNProto(extData []byte, fn func([]byte) error) error {
body, err := vectorU16(extData)
if err != nil {
return err
}
for off := 0; off < len(body); {
n := int(body[off])
off++
if n == 0 {
return lneto.ErrInvalidLengthField
} else if n > len(body)-off {
return lneto.ErrTruncatedFrame
}
err = fn(body[off : off+n])
if err != nil {
return err
}
off += n
}
return nil
}
// ForEachServerName walks the server_name_list of an SNI extension:
//
// struct {
// NameType name_type; // 1 byte, 0 = host_name
// opaque HostName<1..2^16-1>;
// } ServerName;
func ForEachServerName(extData []byte, fn func(nameType uint8, name []byte) error) error {
body, err := vectorU16(extData)
if err != nil {
return err
}
for off := 0; off < len(body); {
if len(body)-off < 3 {
return lneto.ErrTruncatedFrame
}
nameType := body[off]
n := int(binary.BigEndian.Uint16(body[off+1 : off+3]))
off += 3
if n > len(body)-off {
return lneto.ErrTruncatedFrame
}
err = fn(nameType, body[off:off+n])
if err != nil {
return err
}
off += n
}
return nil
}
// vectorU16 strips a two-byte length prefix and returns the vector contents.
// It requires the prefix to describe the buffer exactly: trailing bytes mean
// the sender and this parser disagree on the structure, which is precisely the
// ambiguity parser-differential attacks exploit.
func vectorU16(b []byte) ([]byte, error) {
if len(b) < 2 {
return nil, lneto.ErrTruncatedFrame
}
n := int(binary.BigEndian.Uint16(b[0:2]))
if n != len(b)-2 {
if n > len(b)-2 {
return nil, lneto.ErrTruncatedFrame
}
return nil, errTrailingBytes
}
return b[2:], nil
}
// vectorU8 strips a one-byte length prefix. See [vectorU16] for why trailing
// bytes are rejected.
func vectorU8(b []byte) ([]byte, error) {
if len(b) < 1 {
return nil, lneto.ErrTruncatedFrame
}
n := int(b[0])
if n != len(b)-1 {
if n > len(b)-1 {
return nil, lneto.ErrTruncatedFrame
}
return nil, errTrailingBytes
}
return b[1:], nil
}
+418
View File
@@ -0,0 +1,418 @@
package tls_test
import (
"errors"
"testing"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/tls"
)
func TestRecordFrameHeaderBeforeBody(t *testing.T) {
// A record layer sees the 5 byte header first and must be able to consult
// the length to know how much more to read.
hdr := []byte{22, 0x03, 0x01, 0x00, 0x10}
rf, err := tls.NewRecordFrame(hdr)
if err != nil {
t.Fatal(err)
}
if rf.ContentType() != tls.ContentTypeHandshake {
t.Errorf("content type got %v want handshake", rf.ContentType())
}
if rf.Length() != 16 {
t.Errorf("length got %d want 16", rf.Length())
}
if rf.RecordLength() != 21 {
t.Errorf("record length got %d want 21", rf.RecordLength())
}
if rf.Complete() {
t.Error("record with header only reported complete")
}
if rf.Payload() != nil {
t.Error("payload of incomplete record must be nil, not a short slice")
}
full := append(hdr, make([]byte, 16)...)
rf, err = tls.NewRecordFrame(full)
if err != nil {
t.Fatal(err)
}
if !rf.Complete() {
t.Fatal("full record reported incomplete")
}
if len(rf.Payload()) != 16 {
t.Errorf("payload len got %d want 16", len(rf.Payload()))
}
}
func TestRecordFrameShortHeader(t *testing.T) {
for n := range tls.SizeHeaderRecord {
_, err := tls.NewRecordFrame(make([]byte, n))
if !errors.Is(err, lneto.ErrTruncatedFrame) {
t.Errorf("len=%d got %v want ErrTruncatedFrame", n, err)
}
}
}
func TestRecordFrameRejectsOversizeLength(t *testing.T) {
// The length field is attacker controlled and must be refused before it is
// ever used to size a read.
hdr := []byte{23, 0x03, 0x03, 0xff, 0xff} // 65535 > MaxCiphertext
rf, err := tls.NewRecordFrame(hdr)
if err != nil {
t.Fatal(err)
}
var v lneto.Validator
rf.ValidateSize(&v)
if err := v.ErrPop(); !errors.Is(err, lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", err)
}
// Exactly at the limit is legal.
maxCT := uint16(tls.MaxCiphertext)
hdr[3], hdr[4] = byte(maxCT>>8), byte(maxCT)
rf, _ = tls.NewRecordFrame(hdr)
rf.ValidateSize(&v)
if err := v.ErrPop(); err != nil {
t.Errorf("MaxCiphertext rejected: %v", err)
}
}
func TestInnerPlaintextStripsPadding(t *testing.T) {
for _, tc := range []struct {
name string
in []byte
content string
ctype tls.ContentType
padding int
}{
{"no padding", []byte{'h', 'i', 23}, "hi", tls.ContentTypeApplicationData, 0},
{"padded", []byte{'h', 'i', 23, 0, 0, 0}, "hi", tls.ContentTypeApplicationData, 3},
{"empty content", []byte{22}, "", tls.ContentTypeHandshake, 0},
{"content ends in zero", []byte{'a', 0, 'b', 21, 0}, "a\x00b", tls.ContentTypeAlert, 1},
} {
t.Run(tc.name, func(t *testing.T) {
ip, err := tls.NewInnerPlaintext(tc.in)
if err != nil {
t.Fatal(err)
}
if string(ip.Content()) != tc.content {
t.Errorf("content got %q want %q", ip.Content(), tc.content)
}
if ip.ContentType() != tc.ctype {
t.Errorf("type got %v want %v", ip.ContentType(), tc.ctype)
}
if ip.PaddingLen() != tc.padding {
t.Errorf("padding got %d want %d", ip.PaddingLen(), tc.padding)
}
})
}
}
func TestInnerPlaintextAllZeroRejected(t *testing.T) {
// An all-zero inner plaintext carries no content type. Silently treating it
// as empty would let a peer inject records the state machine cannot
// classify; RFC 8446 5.4 requires unexpected_message.
for _, n := range []int{0, 1, 8} {
_, err := tls.NewInnerPlaintext(make([]byte, n))
if err == nil {
t.Errorf("len=%d: all-zero plaintext accepted", n)
}
}
}
func TestHandshakeFrame24BitLength(t *testing.T) {
hdr := []byte{1, 0x01, 0x02, 0x03} // ClientHello, length 0x010203
hf, err := tls.NewHandshakeFrame(hdr)
if err != nil {
t.Fatal(err)
}
if hf.MsgType() != tls.HandshakeTypeClientHello {
t.Errorf("type got %v", hf.MsgType())
}
if hf.Length() != 0x010203 {
t.Errorf("length got %#x want 0x010203", hf.Length())
}
if hf.Complete() {
t.Error("header-only message reported complete")
}
if hf.Body() != nil {
t.Error("body of incomplete message must be nil")
}
}
func TestHandshakeFrameRoundTripLength(t *testing.T) {
buf := make([]byte, tls.SizeHeaderHandshake)
hf, err := tls.NewHandshakeFrame(buf)
if err != nil {
t.Fatal(err)
}
for _, n := range []int32{0, 1, 255, 256, 65535, 65536, 0xffffff} {
hf.SetLength(n)
if got := hf.Length(); got != n {
t.Errorf("SetLength(%d) round-tripped to %d", n, got)
}
}
}
func TestHandshakeFrameRawDataIncludesHeader(t *testing.T) {
// The transcript hash covers the header plus body, exactly once per
// message. Getting this wrong breaks Finished verification.
msg := []byte{20, 0, 0, 2, 0xaa, 0xbb}
hf, err := tls.NewHandshakeFrame(msg)
if err != nil {
t.Fatal(err)
}
if got := hf.RawData(); len(got) != 6 {
t.Errorf("RawData len got %d want 6", len(got))
}
if got := hf.Body(); len(got) != 2 || got[0] != 0xaa {
t.Errorf("Body got % x", got)
}
}
func TestForEachExtensionWalksAndToleratesGREASE(t *testing.T) {
// Two extensions: a GREASE type with empty data, then supported_versions.
exts := []byte{
0x0a, 0x0a, 0x00, 0x00, // GREASE, len 0
0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04, // supported_versions
}
var types []tls.ExtensionType
err := tls.ForEachExtension(exts, func(ext tls.ExtensionType, data []byte) error {
types = append(types, ext)
return nil
})
if err != nil {
t.Fatal(err)
}
if len(types) != 2 || types[1] != tls.ExtSupportedVersions {
t.Fatalf("got %v", types)
}
if !tls.IsGREASE(uint16(types[0])) {
t.Errorf("first extension %#x not recognized as GREASE", types[0])
}
}
func TestForEachExtensionTruncated(t *testing.T) {
for _, tc := range [][]byte{
{0x00}, // partial type
{0x00, 0x2b, 0x00}, // partial length
{0x00, 0x2b, 0x00, 0x05, 0x02, 0x03}, // length overruns
} {
err := tls.ForEachExtension(tc, func(tls.ExtensionType, []byte) error { return nil })
if !errors.Is(err, lneto.ErrTruncatedFrame) {
t.Errorf("% x: got %v want ErrTruncatedFrame", tc, err)
}
}
}
func TestForEachExtensionPropagatesCallbackError(t *testing.T) {
sentinel := errors.New("stop")
exts := []byte{0x00, 0x2b, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00}
n := 0
err := tls.ForEachExtension(exts, func(tls.ExtensionType, []byte) error {
n++
return sentinel
})
if !errors.Is(err, sentinel) {
t.Errorf("got %v want sentinel", err)
}
if n != 1 {
t.Errorf("walk continued after callback error: %d calls", n)
}
}
func TestForEachKeyShareAcceptsGREASEEntry(t *testing.T) {
// Chrome sends a GREASE key share whose key_exchange is a single byte.
// Rejecting it as malformed breaks Chrome outright.
extData := []byte{
0x00, 0x0b, // client_shares length 11
0x1a, 0x1a, 0x00, 0x01, 0x00, // GREASE group, 1 byte body
0x00, 0x1d, 0x00, 0x02, 0xab, 0xcd, // x25519, 2 byte body
}
type share struct {
g tls.NamedGroup
n int
}
var got []share
err := tls.ForEachKeyShare(extData, func(g tls.NamedGroup, key []byte) error {
got = append(got, share{g, len(key)})
return nil
})
if err != nil {
t.Fatal(err)
}
if len(got) != 2 {
t.Fatalf("got %d shares want 2", len(got))
}
if !tls.IsGREASE(uint16(got[0].g)) || got[0].n != 1 {
t.Errorf("GREASE share mishandled: %+v", got[0])
}
if got[1].g != tls.GroupX25519 || got[1].n != 2 {
t.Errorf("x25519 share mishandled: %+v", got[1])
}
}
func TestForEachALPNProtoRejectsEmptyName(t *testing.T) {
// A zero-length protocol name would make the walk unable to advance.
extData := []byte{0x00, 0x01, 0x00}
err := tls.ForEachALPNProto(extData, func([]byte) error { return nil })
if !errors.Is(err, lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", err)
}
}
func TestForEachALPNProto(t *testing.T) {
extData := []byte{
0x00, 0x0c,
0x02, 'h', '2',
0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
}
var names []string
err := tls.ForEachALPNProto(extData, func(b []byte) error {
names = append(names, string(b))
return nil
})
if err != nil {
t.Fatal(err)
}
if len(names) != 2 || names[0] != "h2" || names[1] != "http/1.1" {
t.Errorf("got %q", names)
}
}
func TestForEachSupportedVersionUsesU8Prefix(t *testing.T) {
// supported_versions is the one hello list with a single byte prefix.
extData := []byte{0x04, 0x1a, 0x1a, 0x03, 0x04}
var vers []uint16
err := tls.ForEachSupportedVersion(extData, func(v uint16) error {
vers = append(vers, v)
return nil
})
if err != nil {
t.Fatal(err)
}
if len(vers) != 2 || vers[1] != tls.VersionTLS13 {
t.Errorf("got %#x", vers)
}
}
func TestVectorRejectsTrailingBytes(t *testing.T) {
// A prefix that under-describes its buffer leaves bytes whose meaning this
// parser and a middlebox could disagree about.
err := tls.ForEachSupportedGroup([]byte{0x00, 0x02, 0x00, 0x1d, 0xff}, func(tls.NamedGroup) error { return nil })
if err == nil {
t.Error("trailing bytes after vector accepted")
}
}
func TestForEachU16RejectsOddLength(t *testing.T) {
err := tls.ForEachU16([]byte{0x00, 0x1d, 0x00}, func(uint16) error { return nil })
if !errors.Is(err, lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", err)
}
}
func TestIsGREASE(t *testing.T) {
// The 16 reserved values of RFC 8701.
for i := range 16 {
v := uint16(i)<<12 | 0x0a00 | uint16(i)<<4 | 0x0a
if !tls.IsGREASE(v) {
t.Errorf("%#04x not detected as GREASE", v)
}
}
for _, v := range []uint16{
0x0000, 0x1301, 0x001d, 0x0403, 0x0a0b, 0x0b0a, 0x1a2a, 0xffff,
} {
if tls.IsGREASE(v) {
t.Errorf("%#04x falsely detected as GREASE", v)
}
}
}
func FuzzNewRecordFrame(f *testing.F) {
f.Add([]byte{22, 3, 1, 0, 5, 1, 2, 3, 4, 5})
f.Add([]byte{23, 3, 3, 0xff, 0xff})
f.Fuzz(func(t *testing.T, b []byte) {
rf, err := tls.NewRecordFrame(b)
if err != nil {
return
}
var v lneto.Validator
rf.ValidateSize(&v)
// Accessors must stay in bounds regardless of validation outcome.
_ = rf.ContentType()
_ = rf.LegacyVersion()
_ = rf.Length()
if p := rf.Payload(); p != nil && len(p) != int(rf.Length()) {
t.Fatalf("payload len %d != declared %d", len(p), rf.Length())
}
if raw := rf.RawData(); len(raw) > len(b) {
t.Fatalf("RawData %d longer than input %d", len(raw), len(b))
}
})
}
func FuzzNewHandshakeFrame(f *testing.F) {
f.Add([]byte{1, 0, 0, 2, 3, 4})
f.Add([]byte{20, 0xff, 0xff, 0xff})
f.Fuzz(func(t *testing.T, b []byte) {
hf, err := tls.NewHandshakeFrame(b)
if err != nil {
return
}
var v lneto.Validator
hf.ValidateSize(&v)
if hf.Length() < 0 {
t.Fatalf("negative 24-bit length %d", hf.Length())
}
if body := hf.Body(); body != nil && len(body) != int(hf.Length()) {
t.Fatalf("body len %d != declared %d", len(body), hf.Length())
}
if raw := hf.RawData(); len(raw) > len(b) {
t.Fatalf("RawData %d longer than input %d", len(raw), len(b))
}
})
}
func FuzzNewInnerPlaintext(f *testing.F) {
f.Add([]byte{1, 2, 23, 0, 0})
f.Add([]byte{0, 0, 0})
f.Fuzz(func(t *testing.T, b []byte) {
ip, err := tls.NewInnerPlaintext(b)
if err != nil {
return
}
if ip.ContentType() == 0 {
t.Fatal("accepted a zero content type")
}
if len(ip.Content())+ip.PaddingLen()+1 != len(b) {
t.Fatalf("content %d + padding %d + 1 != input %d",
len(ip.Content()), ip.PaddingLen(), len(b))
}
})
}
func FuzzForEachExtension(f *testing.F) {
f.Add([]byte{0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04})
f.Add([]byte{0x0a, 0x0a, 0x00, 0x00})
f.Fuzz(func(t *testing.T, b []byte) {
total := 0
err := tls.ForEachExtension(b, func(ext tls.ExtensionType, data []byte) error {
total += 4 + len(data)
if total > len(b) {
t.Fatalf("walked %d bytes past input length %d", total, len(b))
}
// Sub-walkers must also never escape their slice.
_ = tls.ForEachKeyShare(data, func(tls.NamedGroup, []byte) error { return nil })
_ = tls.ForEachALPNProto(data, func([]byte) error { return nil })
_ = tls.ForEachSupportedGroup(data, func(tls.NamedGroup) error { return nil })
_ = tls.ForEachSupportedVersion(data, func(uint16) error { return nil })
_ = tls.ForEachServerName(data, func(uint8, []byte) error { return nil })
return nil
})
if err == nil && total != len(b) {
t.Fatalf("clean walk consumed %d of %d bytes", total, len(b))
}
})
}
+266
View File
@@ -0,0 +1,266 @@
package tls
import (
"encoding/binary"
"github.com/soypat/lneto"
)
// SizeRandom is the length of the client_random and server_random fields.
const SizeRandom = 32
// MaxSessionIDLen is the largest legal legacy_session_id. TLS 1.3 does not use
// session IDs, but a client sends a 32-byte one to trigger middlebox
// compatibility mode and the server must echo it verbatim.
const MaxSessionIDLen = 32
// ClientHelloFrame provides zero-copy access to a ClientHello body
// (RFC 8446 4.1.2). The frame wraps the handshake message *body*, that is the
// bytes returned by [HandshakeFrame.Body], not the handshake header.
//
// struct {
// ProtocolVersion legacy_version = 0x0303;
// Random random; // 32 bytes
// opaque legacy_session_id<0..32>;
// CipherSuite cipher_suites<2..2^16-2>;
// opaque legacy_compression_methods<1..2^8-1>;
// Extension extensions<8..2^16-1>;
// } ClientHello;
//
// Every variable-length field is bounds-checked once by
// [NewClientHelloFrame], so the accessors cannot slice out of range.
type ClientHelloFrame struct {
buf []byte
// Offsets of each variable-length field's contents, resolved once at
// construction so accessors stay branch-free.
sessionIDOff, sessionIDLen int
suitesOff, suitesLen int
compOff, compLen int
extsOff, extsLen int
}
// NewClientHelloFrame parses the structure of a ClientHello body, validating
// that every length prefix is consistent with the buffer. It performs no
// policy checks: version negotiation, cipher suite selection and extension
// validation are the handshake state machine's job.
//
// Trailing bytes after the extensions block are rejected. A sender and parser
// that disagree about where a structure ends is the ambiguity that
// parser-differential attacks are built on.
func NewClientHelloFrame(body []byte) (ClientHelloFrame, error) {
var ch ClientHelloFrame
// legacy_version(2) + random(32) + session_id length(1)
const fixed = 2 + SizeRandom + 1
if len(body) < fixed {
return ch, lneto.ErrTruncatedFrame
}
off := 2 + SizeRandom
sidLen := int(body[off])
off++
if sidLen > MaxSessionIDLen {
// Bounds-checked before it can reach a fixed [32]byte echo buffer.
return ch, lneto.ErrInvalidLengthField
} else if sidLen > len(body)-off {
return ch, lneto.ErrTruncatedFrame
}
ch.sessionIDOff, ch.sessionIDLen = off, sidLen
off += sidLen
if len(body)-off < 2 {
return ch, lneto.ErrTruncatedFrame
}
suitesLen := int(binary.BigEndian.Uint16(body[off : off+2]))
off += 2
if suitesLen > len(body)-off {
return ch, lneto.ErrTruncatedFrame
} else if suitesLen%2 != 0 || suitesLen == 0 {
return ch, lneto.ErrInvalidLengthField
}
ch.suitesOff, ch.suitesLen = off, suitesLen
off += suitesLen
if len(body)-off < 1 {
return ch, lneto.ErrTruncatedFrame
}
compLen := int(body[off])
off++
if compLen > len(body)-off {
return ch, lneto.ErrTruncatedFrame
}
ch.compOff, ch.compLen = off, compLen
off += compLen
// TLS 1.3 requires extensions; a ClientHello without them cannot possibly
// carry supported_versions and so cannot be a 1.3 hello.
if len(body)-off < 2 {
return ch, lneto.ErrTruncatedFrame
}
extsLen := int(binary.BigEndian.Uint16(body[off : off+2]))
off += 2
if extsLen > len(body)-off {
return ch, lneto.ErrTruncatedFrame
} else if extsLen != len(body)-off {
return ch, errTrailingBytes
}
ch.extsOff, ch.extsLen = off, extsLen
ch.buf = body
return ch, nil
}
// LegacyVersion returns the legacy_version field, which TLS 1.3 pins to
// 0x0303 regardless of the version actually negotiated.
func (ch ClientHelloFrame) LegacyVersion() uint16 {
return binary.BigEndian.Uint16(ch.buf[0:2])
}
// Random returns the 32-byte client_random.
func (ch ClientHelloFrame) Random() *[SizeRandom]byte {
return (*[SizeRandom]byte)(ch.buf[2 : 2+SizeRandom])
}
// LegacySessionID returns the legacy_session_id, at most 32 bytes. A TLS 1.3
// server must echo this verbatim in its ServerHello; a non-empty value means
// the client is using middlebox compatibility mode and expects a dummy
// ChangeCipherSpec record.
func (ch ClientHelloFrame) LegacySessionID() []byte {
return ch.buf[ch.sessionIDOff : ch.sessionIDOff+ch.sessionIDLen]
}
// CipherSuites returns the cipher_suites vector with its length prefix
// stripped, ready for [ForEachU16]. The list includes GREASE values.
func (ch ClientHelloFrame) CipherSuites() []byte {
return ch.buf[ch.suitesOff : ch.suitesOff+ch.suitesLen]
}
// LegacyCompressionMethods returns the legacy_compression_methods vector
// contents. For a TLS 1.3 hello this must be exactly one zero byte; see
// [ClientHelloFrame.ValidateCompression].
func (ch ClientHelloFrame) LegacyCompressionMethods() []byte {
return ch.buf[ch.compOff : ch.compOff+ch.compLen]
}
// Extensions returns the extensions block contents with the outer length
// prefix stripped, ready for [ForEachExtension].
func (ch ClientHelloFrame) Extensions() []byte {
return ch.buf[ch.extsOff : ch.extsOff+ch.extsLen]
}
// RawData returns the whole ClientHello body.
func (ch ClientHelloFrame) RawData() []byte { return ch.buf }
// ForEachExtension walks this hello's extensions, rejecting a repeated known
// extension type with [lneto.ErrInvalidField]. RFC 8446 4.2 forbids duplicates,
// and tolerating them invites parser-differential attacks in which this parser
// and a middlebox act on different copies of the same extension.
//
// Unknown and GREASE extension types are passed through without duplicate
// checking, since they are skipped rather than acted upon. Prefer this over
// the bare [ForEachExtension] when parsing an untrusted hello.
func (ch ClientHelloFrame) ForEachExtension(fn func(ExtensionType, []byte) error) error {
var seen extSeen
return ForEachExtension(ch.Extensions(), func(ext ExtensionType, data []byte) error {
if seen.mark(ext) {
return lneto.ErrInvalidField
}
return fn(ext, data)
})
}
// ValidateSize adds an error to v if the ClientHello is structurally invalid.
// Since [NewClientHelloFrame] already rejects every inconsistent length, this
// only re-checks that the frame was successfully constructed.
func (ch ClientHelloFrame) ValidateSize(v *lneto.Validator) {
if ch.buf == nil {
v.AddError(lneto.ErrTruncatedFrame)
}
}
// ValidateCompression reports whether legacy_compression_methods is exactly
// the single null method TLS 1.3 mandates (RFC 8446 4.1.2). Anything else must
// be rejected with an illegal_parameter alert: a client offering real
// compression methods is either pre-1.3 or attempting a downgrade.
func (ch ClientHelloFrame) ValidateCompression() bool {
return ch.compLen == 1 && ch.buf[ch.compOff] == 0
}
// extSeen tracks which known extension types have already been encountered in
// a single hello. RFC 8446 4.2 forbids a duplicate extension type, and
// tolerating duplicates invites parser-differential attacks where this parser
// and a middlebox act on different copies.
//
// Only known types are tracked; unknown and GREASE types are exempt because
// they are skipped without being acted upon.
type extSeen uint64
// mark records ext and reports whether it had already been seen. Extension
// types with no assigned bit are never reported as duplicates.
func (s *extSeen) mark(ext ExtensionType) (duplicate bool) {
bit, ok := extBit(ext)
if !ok {
return false
}
if *s&bit != 0 {
return true
}
*s |= bit
return false
}
// extBit maps an extension type to its bit in [extSeen]. The mapping covers
// every extension this server reads or must reject a duplicate of.
func extBit(ext ExtensionType) (extSeen, bool) {
var i uint
switch ext {
case ExtServerName:
i = 0
case ExtMaxFragmentLength:
i = 1
case ExtStatusRequest:
i = 2
case ExtSupportedGroups:
i = 3
case ExtSignatureAlgorithms:
i = 4
case ExtALPN:
i = 5
case ExtSignedCertificateTimestamp:
i = 6
case ExtPadding:
i = 7
case ExtExtendedMasterSecret:
i = 8
case ExtCompressCertificate:
i = 9
case ExtRecordSizeLimit:
i = 10
case ExtSessionTicket:
i = 11
case ExtPreSharedKey:
i = 12
case ExtEarlyData:
i = 13
case ExtSupportedVersions:
i = 14
case ExtCookie:
i = 15
case ExtPSKKeyExchangeModes:
i = 16
case ExtCertificateAuthorities:
i = 17
case ExtSignatureAlgorithmsCert:
i = 18
case ExtKeyShare:
i = 19
case ExtApplicationSettings:
i = 20
case ExtEncryptedClientHello:
i = 21
case ExtRenegotiationInfo:
i = 22
default:
return 0, false
}
return 1 << i, true
}
+289
View File
@@ -0,0 +1,289 @@
package tls_test
import (
"crypto/tls"
"errors"
"net"
"testing"
"time"
"github.com/soypat/lneto"
ltls "github.com/soypat/lneto/x/tls"
)
// captureClientHello drives a standard library TLS client far enough to emit
// its first flight and returns the ClientHello handshake message body.
//
// Using a real client rather than a hand-written fixture means the parser is
// exercised against genuine extension ordering, a 32-byte compatibility
// session ID and GREASE-free but otherwise realistic content. Browser captures
// arrive at Stage 6; this covers the structure in the meantime.
func captureClientHello(t *testing.T) ltls.ClientHelloFrame {
t.Helper()
client, server := net.Pipe()
defer client.Close()
defer server.Close()
go func() {
c := tls.Client(client, &tls.Config{
ServerName: "example.com",
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
NextProtos: []string{"h2", "http/1.1"},
})
_ = c.Handshake() // will fail; we only need the first flight
}()
server.SetReadDeadline(time.Now().Add(10 * time.Second))
var buf [4096]byte
n, err := server.Read(buf[:])
if err != nil {
t.Fatalf("reading ClientHello: %v", err)
}
rec, err := ltls.NewRecordFrame(buf[:n])
if err != nil {
t.Fatalf("record: %v", err)
}
if rec.ContentType() != ltls.ContentTypeHandshake {
t.Fatalf("first record is %v, want handshake", rec.ContentType())
}
if !rec.Complete() {
t.Fatalf("ClientHello record split across reads: have %d want %d",
n, rec.RecordLength())
}
hs, err := ltls.NewHandshakeFrame(rec.Payload())
if err != nil {
t.Fatalf("handshake: %v", err)
}
if hs.MsgType() != ltls.HandshakeTypeClientHello {
t.Fatalf("first message is %v, want client_hello", hs.MsgType())
}
if !hs.Complete() {
t.Fatal("ClientHello spans multiple records")
}
ch, err := ltls.NewClientHelloFrame(hs.Body())
if err != nil {
t.Fatalf("client hello: %v", err)
}
return ch
}
func TestClientHelloParseRealHello(t *testing.T) {
ch := captureClientHello(t)
if ch.LegacyVersion() != ltls.VersionTLS12 {
t.Errorf("legacy_version %#04x want 0x0303", ch.LegacyVersion())
}
if !ch.ValidateCompression() {
t.Errorf("legacy_compression_methods % x, want exactly {0}",
ch.LegacyCompressionMethods())
}
if n := len(ch.LegacySessionID()); n != 32 {
// A TLS 1.3 client sends a fake 32-byte session ID to trigger
// middlebox compatibility mode. The server must echo it.
t.Errorf("session id len %d, want 32 for middlebox compat", n)
}
var sawTLS13, sawX25519, sawSNI, sawALPN bool
var suites []ltls.CipherSuite
err := ch.ForEachExtension(func(ext ltls.ExtensionType, data []byte) error {
switch ext {
case ltls.ExtSupportedVersions:
return ltls.ForEachSupportedVersion(data, func(v uint16) error {
if v == ltls.VersionTLS13 {
sawTLS13 = true
}
return nil
})
case ltls.ExtKeyShare:
return ltls.ForEachKeyShare(data, func(g ltls.NamedGroup, key []byte) error {
if g == ltls.GroupX25519 && len(key) == 32 {
sawX25519 = true
}
return nil
})
case ltls.ExtServerName:
return ltls.ForEachServerName(data, func(nameType uint8, name []byte) error {
if nameType == 0 && string(name) == "example.com" {
sawSNI = true
}
return nil
})
case ltls.ExtALPN:
return ltls.ForEachALPNProto(data, func(p []byte) error {
if string(p) == "http/1.1" {
sawALPN = true
}
return nil
})
}
return nil
})
if err != nil {
t.Fatalf("walking extensions: %v", err)
}
if err := ltls.ForEachU16(ch.CipherSuites(), func(v uint16) error {
suites = append(suites, ltls.CipherSuite(v))
return nil
}); err != nil {
t.Fatalf("walking cipher suites: %v", err)
}
if !sawTLS13 {
t.Error("supported_versions did not offer TLS 1.3")
}
if !sawX25519 {
t.Error("no 32-byte x25519 key share found")
}
if !sawSNI {
t.Error("SNI host not recovered")
}
if !sawALPN {
t.Error("ALPN http/1.1 not recovered")
}
var mandatory bool
for _, s := range suites {
if s == ltls.SuiteAES128GCMSHA256 {
mandatory = true
}
}
if !mandatory {
t.Errorf("TLS_AES_128_GCM_SHA256 not offered; got %v", suites)
}
}
func TestClientHelloRejectsDuplicateExtension(t *testing.T) {
// Duplicating supported_versions must be caught. Tolerating it lets this
// parser and a middlebox act on different copies.
ch := captureClientHello(t)
exts := ch.Extensions()
// Find the first extension and append a verbatim copy of it.
var first []byte
err := ltls.ForEachExtension(exts, func(ext ltls.ExtensionType, data []byte) error {
if first == nil {
first = make([]byte, 4+len(data))
copy(first, exts)
}
return nil
})
if err != nil || first == nil {
t.Fatalf("could not isolate first extension: %v", err)
}
dup := make([]byte, 0, len(exts)+len(first))
dup = append(dup, exts...)
dup = append(dup, first...)
body := rebuildHelloWithExtensions(t, ch, dup)
ch2, err := ltls.NewClientHelloFrame(body)
if err != nil {
t.Fatalf("rebuilt hello did not parse: %v", err)
}
err = ch2.ForEachExtension(func(ltls.ExtensionType, []byte) error { return nil })
if !errors.Is(err, lneto.ErrInvalidField) {
t.Errorf("duplicate extension got %v, want ErrInvalidField", err)
}
}
// rebuildHelloWithExtensions re-encodes ch with a replacement extensions block,
// exercising Builder against a structure produced by a real client.
func rebuildHelloWithExtensions(t *testing.T, ch ltls.ClientHelloFrame, exts []byte) []byte {
t.Helper()
var b ltls.Builder
b.Reset(make([]byte, 0, len(ch.RawData())+len(exts)+64))
b.AddU16(ch.LegacyVersion())
b.AddBytes(ch.Random()[:])
b.OpenU8()
b.AddBytes(ch.LegacySessionID())
b.Close()
b.OpenU16()
b.AddBytes(ch.CipherSuites())
b.Close()
b.OpenU8()
b.AddBytes(ch.LegacyCompressionMethods())
b.Close()
b.OpenU16()
b.AddBytes(exts)
b.Close()
out, err := b.Bytes()
if err != nil {
t.Fatalf("rebuilding hello: %v", err)
}
return out
}
func TestClientHelloRejectsOversizeSessionID(t *testing.T) {
// legacy_session_id feeds a fixed [32]byte echo buffer in the server, so
// the bound must be enforced at parse time.
body := make([]byte, 0, 128)
body = append(body, 0x03, 0x03)
body = append(body, make([]byte, ltls.SizeRandom)...)
body = append(body, 33) // session id length, one over
body = append(body, make([]byte, 33)...) //
body = append(body, 0x00, 0x02, 0x13, 0x01) // cipher suites
body = append(body, 0x01, 0x00) // compression
body = append(body, 0x00, 0x00) // extensions, empty
_, err := ltls.NewClientHelloFrame(body)
if !errors.Is(err, lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", err)
}
}
func TestClientHelloRejectsTrailingBytes(t *testing.T) {
ch := captureClientHello(t)
body := append(append([]byte{}, ch.RawData()...), 0xff)
if _, err := ltls.NewClientHelloFrame(body); err == nil {
t.Error("trailing byte after extensions block accepted")
}
}
func TestClientHelloTruncatedAtEveryOffset(t *testing.T) {
// Truncating a valid hello anywhere must produce an error, never a panic
// and never a frame whose accessors read out of bounds.
ch := captureClientHello(t)
full := ch.RawData()
for n := range len(full) {
frame, err := ltls.NewClientHelloFrame(full[:n])
if err == nil {
// A shorter prefix must never parse as a complete hello.
t.Errorf("truncation to %d/%d bytes parsed clean", n, len(full))
_ = frame.Extensions()
}
}
}
func FuzzNewClientHelloFrame(f *testing.F) {
f.Add([]byte{
0x03, 0x03,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0x00, // no session id
0x00, 0x02, 0x13, 0x01, // one cipher suite
0x01, 0x00, // null compression
0x00, 0x00, // no extensions
})
f.Fuzz(func(t *testing.T, b []byte) {
ch, err := ltls.NewClientHelloFrame(b)
if err != nil {
return
}
// Every accessor must stay inside the input.
if len(ch.LegacySessionID()) > ltls.MaxSessionIDLen {
t.Fatalf("session id %d bytes exceeds max", len(ch.LegacySessionID()))
}
if len(ch.CipherSuites())%2 != 0 {
t.Fatal("cipher suites vector has odd length")
}
total := 2 + ltls.SizeRandom + 1 + len(ch.LegacySessionID()) +
2 + len(ch.CipherSuites()) +
1 + len(ch.LegacyCompressionMethods()) +
2 + len(ch.Extensions())
if total != len(b) {
t.Fatalf("fields sum to %d but input is %d bytes", total, len(b))
}
_ = ch.ValidateCompression()
_ = ch.ForEachExtension(func(ltls.ExtensionType, []byte) error { return nil })
})
}
+3 -28
View File
@@ -69,8 +69,6 @@ type StackAsync struct {
ipv6enabled bool
stack6 Stack6
log *slog.Logger
}
type StackConfig struct {
@@ -107,9 +105,6 @@ type StackConfig struct {
AcceptMulticast bool
// Accept broadcast IPv4 packets. Needed for managing access points and DHCPv4 servers.
AcceptIPv4Broadcast bool
// Logger receives the stack's Debug and DebugErr output. A nil Logger silences
// them; the heap allocation probe still runs so allocation bisection keeps working.
Logger *slog.Logger
}
func (cfg *StackConfig) id() uint16 {
@@ -207,7 +202,6 @@ func (s *StackAsync) Reset(cfg StackConfig) (err error) {
defer s.mu.Unlock()
s.prng = uint32(cfg.RandSeed)
s.hostname = cfg.Hostname
s.log = cfg.Logger
// Treat last character of hostname as number.
id := cfg.id()
linkNodes := 2 // ARP and IPv4 nodes
@@ -849,39 +843,20 @@ func addr4(addr [4]byte, ok bool) netip.Addr {
}
// Debug prints debugging and heap information.
//
// The heap allocation probe runs unconditionally; only the log line is gated on
// the configured Logger's level. Building the slog.Attr list allocates, so the
// gate must come first or the allocation happens even when nothing is logged.
func (s *StackAsync) Debug(msg string) {
internal.LogAllocs(msg)
if !internal.LogEnabled(s.log, slog.LevelDebug) {
return
}
internal.LogAttrsAndAllocs(msg, s.log, slog.LevelDebug, "stackasync",
internal.LogAttrsAndAllocs(msg, slog.Default(), slog.LevelDebug, "stackasync",
slog.String("umsg", msg),
slog.Uint64("sent", s.stats.TotalSent),
slog.Uint64("recv", s.stats.TotalReceived),
)
}
// DebugErr prints debugging and heap information with [slog.LevelError] level. See [StackAsync.Debug] on gating.
// DebugErr prints debugging and heap information.
func (s *StackAsync) DebugErr(msg, err string) {
internal.LogAllocs(msg)
if !internal.LogEnabled(s.log, slog.LevelError) {
return
}
internal.LogAttrsAndAllocs(msg, s.log, slog.LevelError, "stackasync",
internal.LogAttrsAndAllocs(msg, slog.Default(), slog.LevelError, "stackasync",
slog.String("umsg", msg),
slog.String("err", err),
slog.Uint64("sent", s.stats.TotalSent),
slog.Uint64("recv", s.stats.TotalReceived),
)
}
// LogAllocs is an lneto-tracked allocation logger. If there was an allocation between this call and a previous call
// to LogAllocs it will be printed. This is called globally by StackAsync.Debug methods and by all logging calls in lneto
// when build tag debugheaplog is set.
func LogAllocs(msg string) {
internal.LogAllocs(msg)
}
+1 -1
View File
@@ -32,7 +32,7 @@ func FuzzStackPacketHTTP(f *testing.F) {
}
hdr.SetMethod("GET")
hdr.SetProtocol("HTTP/1.1")
hdr.SetRequestTarget("/")
hdr.SetRequestURI("/")
data := hdr.AppendHeaders(nil)
pktnum := 0