mirror of
https://github.com/soypat/lneto.git
synced 2026-08-21 06:59:04 +00:00
add http/httphi (#171)
* add http/httphi * begin adding httphi tests * claude found neat bugs * add low level Handle function and more tests * more tests, run go generate * add Hijacker-like functionality * improve locking and acquisition of Exchanges in reconfiguring * several bugfixes, add internal.IntLen, round up http-linux example with new router API * small nit * add benchmarks * add query handling * remove ForEach pattern, allocates in TinyGo * massive documentation push and code reordering in files * Router.Handle returns error after being torn down * run go fix * rework Mux interface to receive a string request path * add MethodFrom * minor doc nit * fail on incomplete staging * add raw buffer access * add streaming API distinct from Exchange * begin adding multipart form logic * finish rounding up multipart form parsing * remove status type * first Multipart approach * begin adding readMultiPart * add Exchange.ReadMultiparts reimagining of clanker slop * ai insists with backoffs * simplify clanker slop * apply go fix * add a pattern argument to Mux * explicit header key/value alloc and add ExchangeConfig * fix tests after excplicit header alloc change * fix examples * run go fix * expose rawsock as experimental package (will use for external benchmarks) * remove backoff from form parsing * @MDr164 suggestions get potential fixes * apply go fix * add examples * add README.md * fix rawsock tinygo implementation * apply @MDr164 various fixes * update documentation on ContentLength methods and fix bug in Form reset on empty body * fix tests * add fuzz tests * run go fix * io.ErrNoProgress on parsing form spin * run go fix * remove backoff assumption from Router * httphi.Handle rejects unsupported protocols * go format router.go * add kvbuffer * rewrite Cookie with KVBuffer * mid refactor of KVBuffer into Header * work on KVBuffer exhausted semantics * add Go's ServeMux Request.PathValue access semantics to Exchange, Mux and MuxSlice * add PathValue example * document all the things; improve req Query semantics; add Form.EnableBufferGrowth * unexport kvBuffer * add Exchange.PathValueAppend * use stdlib in example instead of rawsock * remove rawsock from http example * add darwin arch rawsock * fix example * rename Router.TeardownGoroutines to Shutdown matching http.Server.Shutdown * rename types and identifiers * @MDr164 Content-Type and Transfer-Encoding bug catches
This commit is contained in:
committed by
Patricio Whittingslow
parent
a3f2742abf
commit
5c54030f19
+43
-61
@@ -7,105 +7,89 @@ 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 {
|
||||
buf []byte
|
||||
kvs []argsKV // first key-value pair is the data Key/Value pair.
|
||||
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)
|
||||
}
|
||||
|
||||
// Reset functions very similarly to [Header.Reset]. Can be used for in-place cookie parsing.
|
||||
func (c *Cookie) Reset(buf []byte) {
|
||||
if buf == nil {
|
||||
buf = c.buf[:0]
|
||||
}
|
||||
*c = Cookie{
|
||||
buf: buf,
|
||||
kvs: c.kvs[:0],
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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 len(c.kvs) == 0 || c.kvs[0].key.len == 0 {
|
||||
if !c.valid() {
|
||||
return nil
|
||||
}
|
||||
return tok2bytes(c.buf, c.kvs[0].key)
|
||||
return c.kv.AtKey(0)
|
||||
}
|
||||
|
||||
// Value returns the first cookie value associated with the name. Returns nil if not found.
|
||||
func (c *Cookie) Value() []byte {
|
||||
if len(c.kvs) == 0 || c.kvs[0].value.len == 0 {
|
||||
if !c.valid() {
|
||||
return nil
|
||||
}
|
||||
return tok2bytes(c.buf, c.kvs[0].value)
|
||||
return c.kv.AtValue(0)
|
||||
}
|
||||
|
||||
// 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)
|
||||
c.buf = append(c.buf[:0], cookie...)
|
||||
c.Reset(nil, 0)
|
||||
c.kv.buf = append(c.kv.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.buf = append(dst.buf[:0], c.buf...)
|
||||
dst.kvs = append(dst.kvs[:0], c.kvs...)
|
||||
}
|
||||
func (dst *Cookie) CopyFrom(c Cookie) { dst.kv.CopyFrom(&c.kv) }
|
||||
|
||||
// Parse parses the cookie's buffer in place.
|
||||
func (c *Cookie) Parse() error {
|
||||
if len(c.kvs) > 0 {
|
||||
if c.kv.Len() > 0 {
|
||||
return errCookiesParsed
|
||||
}
|
||||
off := 0
|
||||
for {
|
||||
k, v, n := parseCookie(c.buf[off:])
|
||||
k, v, n := parseCookie(c.kv.buf[off:])
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
c.kvs = append(c.kvs, argsKV{
|
||||
key: bytes2tok(c.buf, k),
|
||||
value: bytes2tok(c.buf, v),
|
||||
})
|
||||
if !c.kv.setInternal(k, v) {
|
||||
return ErrBufferExhausted
|
||||
}
|
||||
|
||||
off += n
|
||||
}
|
||||
if len(c.kvs) == 0 {
|
||||
if c.kv.Len() == 0 {
|
||||
return errNoCookies
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
func (c *Cookie) Get(key string) []byte { return c.kv.Get(key) }
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -158,16 +142,14 @@ 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 := len(c.kvs)
|
||||
nc := c.kv.Len()
|
||||
for i := range nc {
|
||||
kv := c.kvs[i]
|
||||
key := tok2bytes(c.buf, kv.key)
|
||||
value := tok2bytes(c.buf, kv.value)
|
||||
if len(key) != 0 {
|
||||
dst = append(dst, key...)
|
||||
k, v := c.kv.At(i)
|
||||
if len(k) != 0 {
|
||||
dst = append(dst, k...)
|
||||
dst = append(dst, '=')
|
||||
}
|
||||
dst = append(dst, value...)
|
||||
dst = append(dst, v...)
|
||||
if i+1 < nc {
|
||||
dst = append(dst, ';', ' ')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package httpraw
|
||||
|
||||
import "bytes"
|
||||
|
||||
// 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.
|
||||
func (f *Form) Reset(buf []byte, capKV int) {
|
||||
f.kv.Reset(buf, capKV)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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)
|
||||
f.Parse()
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Errorf("reused Form allocated %v times, want 0", allocs)
|
||||
}
|
||||
}
|
||||
+274
-181
@@ -3,31 +3,40 @@ package httpraw
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
methodGet = "GET"
|
||||
strHTTP11 = "HTTP/1.1"
|
||||
strCRLF = "\r\n"
|
||||
headerCookie = "Cookie"
|
||||
headerConnection = "Connection"
|
||||
strClose = "close"
|
||||
methodGet = "GET"
|
||||
strHTTP11 = "HTTP/1.1"
|
||||
strCRLF = "\r\n"
|
||||
headerCookie = "Cookie"
|
||||
headerConnection = "Connection"
|
||||
headerContentLength = "Content-Length"
|
||||
strClose = "close"
|
||||
strKeepAlive = "keep-alive"
|
||||
)
|
||||
|
||||
type flags uint16
|
||||
// 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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
func (f flags) hasAny(checkThese flags) bool {
|
||||
// HasAny returns true if any of the argument flags are set.
|
||||
func (f Flags) HasAny(checkThese Flags) bool {
|
||||
return f&checkThese != 0
|
||||
}
|
||||
|
||||
@@ -42,32 +51,34 @@ type Header struct {
|
||||
hbuf headerBuf
|
||||
|
||||
// Request fields.
|
||||
method headerSlice
|
||||
requestURI headerSlice
|
||||
proto headerSlice
|
||||
method view
|
||||
requestTarget view
|
||||
proto view
|
||||
|
||||
// Response fields.
|
||||
statusCode headerSlice
|
||||
statusText headerSlice
|
||||
|
||||
flags flags
|
||||
_ noCopy
|
||||
statusCode view
|
||||
statusText view
|
||||
_ noCopy
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
h.hbuf.readFromBytes(b)
|
||||
h.Reset(nil, 0)
|
||||
err := h.hbuf.kv.ReadFromBytes(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return h.parse(asResponse)
|
||||
}
|
||||
|
||||
@@ -75,7 +86,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.buf)
|
||||
h.Reset(h.hbuf.kv.buf, 0)
|
||||
debuglog("http:parse:start")
|
||||
return h.parse(asResponse)
|
||||
}
|
||||
@@ -97,96 +108,68 @@ func (h *Header) Parse(asResponse bool) error {
|
||||
// return err
|
||||
// }
|
||||
func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) {
|
||||
if h.flags.hasAny(flagDoneParsingHeader) {
|
||||
flags := h.Flags()
|
||||
if flags.HasAny(flagDoneParsingHeader) {
|
||||
return false, errAlreadyParsed
|
||||
} else if h.flags.hasAny(flagMangledBuffer) {
|
||||
} else if flags.HasAny(flagMangledBuffer) {
|
||||
return false, errMangledBuffer
|
||||
}
|
||||
if asResponse && h.statusCode.len == 0 || !asResponse && h.requestURI.start == 0 {
|
||||
if asResponse && h.statusCode.len == 0 || !asResponse && h.requestTarget.start == 0 {
|
||||
err = h.parseFirstLine(asResponse)
|
||||
if err != nil {
|
||||
return err == errNeedMore, err
|
||||
return err == ErrNeedMoreData, err
|
||||
}
|
||||
}
|
||||
err = h.parseNextHeaders()
|
||||
return err == errNeedMore, err
|
||||
err = h.parseNextHeaders(flags)
|
||||
return err == ErrNeedMoreData, 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) {
|
||||
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
|
||||
return h.hbuf.kv.ReadLimited(r, maxBytesToRead)
|
||||
}
|
||||
|
||||
// ReadFromBytes appends argument buffer to underlying buffer.
|
||||
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
|
||||
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
|
||||
func (h *Header) ReadFromBytes(b []byte) error {
|
||||
return h.hbuf.kv.ReadFromBytes(b)
|
||||
}
|
||||
|
||||
// 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.buf)
|
||||
return len(h.hbuf.kv.BufferRaw())
|
||||
}
|
||||
|
||||
// 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.buf)
|
||||
return len(h.hbuf.kv.BufferRaw())
|
||||
}
|
||||
|
||||
// BufferFree returns amount of bytes free in underlying buffer.
|
||||
@@ -200,29 +183,12 @@ func (h *Header) BufferFree() int {
|
||||
//
|
||||
// BufferUsed + BufferFree == BufferCapacity
|
||||
func (h *Header) BufferCapacity() int {
|
||||
return cap(h.hbuf.buf)
|
||||
return cap(h.hbuf.kv.BufferRaw())
|
||||
}
|
||||
|
||||
// ForEach iterates over header key-value field tuples.
|
||||
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
|
||||
func (h *Header) ForEach(cb func(key, value []byte) bool) {
|
||||
h.hbuf.kv.ForEach(cb)
|
||||
}
|
||||
|
||||
// Reset discards all parsed data and sets the buffer data to buf. This method
|
||||
@@ -230,30 +196,28 @@ func (hb *headerBuf) forEach(cb func(key, value []byte) error) error {
|
||||
// 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]); 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(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(nil) // Reuse buffer previously set in a call to Reset.
|
||||
func (h *Header) Reset(buf []byte) {
|
||||
if h.flags.hasAny(flagNoBufferGrow) && cap(buf) < 32 {
|
||||
panic("small buffer and flagNoBufferGrow set")
|
||||
}
|
||||
func (h *Header) Reset(buf []byte, numHeaderCapacity int) {
|
||||
const persistentFlags = flagNoBufferGrow
|
||||
debuglog("http:reset:hbuf")
|
||||
h.hbuf.reset(buf)
|
||||
*h = Header{
|
||||
hbuf: h.hbuf,
|
||||
flags: h.flags & persistentFlags,
|
||||
h.hbuf.reset(buf, numHeaderCapacity)
|
||||
if h.Flags().HasAny(flagNoBufferGrow) && h.BufferCapacity() < 32 {
|
||||
panic("small buffer and flagNoBufferGrow set")
|
||||
}
|
||||
*h = Header{hbuf: h.hbuf}
|
||||
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")
|
||||
if h.flags.hasAny(flagMangledBuffer) {
|
||||
flags := h.Flags()
|
||||
if flags.HasAny(flagMangledBuffer) {
|
||||
return nil, errMangledBuffer
|
||||
} else if h.flags.hasAny(flagDoneParsingHeader) {
|
||||
return h.hbuf.buf[h.hbuf.off:], nil
|
||||
} else if flags.HasAny(flagDoneParsingHeader) {
|
||||
return h.BufferRaw()[h.hbuf.off:], nil
|
||||
}
|
||||
return nil, errUnparsed
|
||||
}
|
||||
@@ -271,63 +235,56 @@ func (h *Header) SetInt(key string, value int64, base int) {
|
||||
if base < 2 || base > 36 {
|
||||
return // strconv.AppendInt only supports base 2..36.
|
||||
}
|
||||
useKv := h.takeReusableSlot(key)
|
||||
if useKv == nil {
|
||||
h.appendHeaderInt(key, value, base)
|
||||
} else {
|
||||
useKv.value = h.reuseOrAppendInt(useKv.value, value, base)
|
||||
}
|
||||
h.hbuf.kv.SetInt(key, value, base)
|
||||
}
|
||||
|
||||
// Set sets a key-value pair in the HTTP header.
|
||||
// Calling Set mangles the buffer.
|
||||
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)
|
||||
}
|
||||
func (h *Header) Set(key, value string) (enoughSpace bool) {
|
||||
return h.hbuf.kv.Set(key, value)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
return useKv
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
debuglog("http:get:notfound")
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
// Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer.
|
||||
func (h *Header) Add(key, value string) {
|
||||
h.appendHeader(key, value)
|
||||
h.hbuf.kv.appendPair(key, value)
|
||||
}
|
||||
|
||||
// Method returns HTTP request method.
|
||||
@@ -337,17 +294,41 @@ func (h *Header) Method() []byte {
|
||||
|
||||
// SetMethod sets the request header's method.
|
||||
func (h *Header) SetMethod(method string) {
|
||||
h.method = h.reuseOrAppend(h.method, method)
|
||||
h.method = h.hbuf.kv.reuseOrAppend(h.method, method)
|
||||
}
|
||||
|
||||
// SetRequestURI sets RequestURI for the first HTTP request line.
|
||||
func (h *Header) SetRequestURI(requestURI string) {
|
||||
h.requestURI = h.reuseOrAppend(h.requestURI, requestURI)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// RequestURI returns RequestURI from the first HTTP request line.
|
||||
func (h *Header) RequestURI() []byte {
|
||||
return h.getNonEmptyValue(h.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
|
||||
}
|
||||
|
||||
// Protocol returns the request header's HTTP protocol. Usually "HTTP/1.1".
|
||||
@@ -357,7 +338,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.reuseOrAppend(h.proto, protocol)
|
||||
h.proto = h.hbuf.kv.reuseOrAppend(h.proto, protocol)
|
||||
}
|
||||
|
||||
// Status returns the response header's status code and status text. i.e: "200" "OK".
|
||||
@@ -365,28 +346,36 @@ func (h *Header) Status() (code, statusText []byte) {
|
||||
if h.statusCode.len == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return h.hbuf.musttoken(h.statusCode), h.hbuf.musttoken(h.statusText)
|
||||
return h.hbuf.kv.musttoken(h.statusCode), h.hbuf.kv.musttoken(h.statusText)
|
||||
}
|
||||
|
||||
// Status sets the response header's status code and status text. i.e: "200" "OK".
|
||||
// SetStatus sets the response header's status code and status text. i.e: "200" "OK".
|
||||
func (h *Header) SetStatus(code, statusText string) {
|
||||
h.statusCode = h.reuseOrAppend(h.statusCode, code)
|
||||
h.statusText = h.reuseOrAppend(h.statusText, statusText)
|
||||
h.hbuf.kv.flags |= FlagStatusSet
|
||||
h.statusCode = h.hbuf.kv.reuseOrAppend(h.statusCode, code)
|
||||
h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText)
|
||||
}
|
||||
|
||||
func (h *Header) getNonEmptyValue(s headerSlice) []byte {
|
||||
// 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 {
|
||||
if s.len == 0 {
|
||||
return nil // If empty then value is invalid, return nil.
|
||||
}
|
||||
return h.hbuf.musttoken(s)
|
||||
return h.hbuf.kv.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.flags.hasAny(flagOOMReached) {
|
||||
return dst, errOOM
|
||||
} else if h.requestURI.len == 0 || h.method.len == 0 {
|
||||
if h.hbuf.kv.flags.HasAny(flagOOMReached) {
|
||||
return dst, ErrBufferExhausted
|
||||
} else if h.requestTarget.len == 0 || h.method.len == 0 {
|
||||
return dst, errNeedMethodURI
|
||||
} else if len(proto) == 0 {
|
||||
return dst, errNoProto
|
||||
@@ -398,7 +387,7 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
|
||||
} else {
|
||||
dst = append(dst, method...)
|
||||
}
|
||||
uri := h.RequestURI()
|
||||
uri := h.RequestTarget()
|
||||
|
||||
dst = append(dst, ' ')
|
||||
dst = append(dst, uri...)
|
||||
@@ -413,9 +402,19 @@ 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.flags.hasAny(flagOOMReached) {
|
||||
return dst, errOOM
|
||||
if h.hbuf.kv.flags.HasAny(flagOOMReached) {
|
||||
return dst, ErrBufferExhausted
|
||||
} else if h.statusCode.len == 0 || h.statusText.len == 0 {
|
||||
return dst, errBadStatusCodeTxt
|
||||
} else if len(proto) == 0 {
|
||||
@@ -429,26 +428,24 @@ func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
|
||||
dst = append(dst, ' ')
|
||||
dst = append(dst, text...)
|
||||
dst = append(dst, strCRLF...)
|
||||
|
||||
dst = h.AppendHeaders(dst)
|
||||
|
||||
return append(dst, strCRLF...), nil
|
||||
return dst, 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, 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))
|
||||
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))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
@@ -540,3 +537,99 @@ 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
|
||||
}
|
||||
|
||||
+301
-23
@@ -2,6 +2,7 @@ package httpraw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const numHeaderCapacity = 16
|
||||
|
||||
func TestHeaderParseRequest(t *testing.T) {
|
||||
const (
|
||||
wantMethod = "GET"
|
||||
@@ -50,8 +53,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.RequestURI(), []byte(wantURI)) {
|
||||
t.Errorf("want request URI %q, got %q", wantURI, hdr.RequestURI())
|
||||
if !bytes.Equal(hdr.RequestTarget(), []byte(wantURI)) {
|
||||
t.Errorf("want request URI %q, got %q", wantURI, hdr.RequestTarget())
|
||||
}
|
||||
contentLength, _ := strconv.Atoi(string(hdr.Get("Content-Length")))
|
||||
if contentLength != len(wantMessage) {
|
||||
@@ -59,7 +62,7 @@ func TestHeaderParseRequest(t *testing.T) {
|
||||
}
|
||||
var c Cookie
|
||||
cookie := hdr.Get("Cookie")
|
||||
c.Reset(cookie)
|
||||
c.Reset(cookie, 0)
|
||||
err = c.Parse()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -151,6 +154,125 @@ 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
|
||||
@@ -209,11 +331,72 @@ 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)
|
||||
h.Reset(nil, numHeaderCapacity)
|
||||
h.SetMethod("GET")
|
||||
h.SetRequestURI("/")
|
||||
h.SetRequestTarget("/")
|
||||
h.SetProtocol("HTTP/1.1")
|
||||
|
||||
h.Set("Host", "first.example.com")
|
||||
@@ -234,7 +417,7 @@ func TestHeaderSetOverwrite(t *testing.T) {
|
||||
|
||||
func TestHeaderSetBytesEmptyValue(t *testing.T) {
|
||||
var h Header
|
||||
h.Reset(nil)
|
||||
h.Reset(nil, numHeaderCapacity)
|
||||
h.SetBytes("X-Empty", nil)
|
||||
if got := h.Get("X-Empty"); len(got) != 0 {
|
||||
t.Errorf("want empty value, got %q", got)
|
||||
@@ -268,7 +451,7 @@ func TestHeader_LargeBufferOverflow(t *testing.T) {
|
||||
}
|
||||
|
||||
// a complete but malformed header line with no colon must be a hard error,
|
||||
// not errNeedMore (which makes a streaming parser wait forever).
|
||||
// not ErrNeedMoreData (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
|
||||
@@ -276,8 +459,8 @@ func TestHeader_ColonlessLineIsHardError(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("want error on colonless header line, got nil")
|
||||
}
|
||||
if err == errNeedMore {
|
||||
t.Fatalf("colonless line reported as errNeedMore (parser would hang); want a hard error like errInvalidName")
|
||||
if err == ErrNeedMoreData {
|
||||
t.Fatalf("colonless line reported as ErrNeedMoreData (parser would hang); want a hard error like errInvalidName")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,19 +472,19 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) {
|
||||
const part2 = ": example.com\r\n\r\n"
|
||||
|
||||
var h Header
|
||||
h.Reset(nil)
|
||||
if _, err := h.ReadFromBytes([]byte(part1)); err != nil {
|
||||
h.Reset(nil, numHeaderCapacity)
|
||||
if err := h.ReadFromBytes([]byte(part1)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
needMore, err := h.TryParse(false)
|
||||
if err != nil && err != errNeedMore {
|
||||
t.Fatalf("split before colon: want errNeedMore/nil, got %v", err)
|
||||
if err != nil && err != ErrNeedMoreData {
|
||||
t.Fatalf("split before colon: want ErrNeedMoreData/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)
|
||||
@@ -322,7 +505,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)
|
||||
h.Reset(buf, numHeaderCapacity)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("appendHeader panicked on exact-cap buffer: %v", r)
|
||||
@@ -339,10 +522,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)
|
||||
h.EnableBufferGrowth(false)
|
||||
h.Reset(buf, numHeaderCapacity)
|
||||
h.ConfigBufferGrowth(false)
|
||||
h.SetMethod("GET")
|
||||
h.SetRequestURI("/")
|
||||
h.SetRequestTarget("/")
|
||||
h.SetProtocol("HTTP/1.1")
|
||||
|
||||
defer func() {
|
||||
@@ -374,7 +557,7 @@ func TestHeader_SetInt(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var h Header
|
||||
h.Reset(nil)
|
||||
h.Reset(nil, numHeaderCapacity)
|
||||
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)
|
||||
@@ -386,9 +569,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)
|
||||
h.Reset(nil, numHeaderCapacity)
|
||||
h.SetMethod("GET")
|
||||
h.SetRequestURI("/")
|
||||
h.SetRequestTarget("/")
|
||||
h.SetProtocol("HTTP/1.1")
|
||||
|
||||
h.SetInt("Content-Length", 100, 10)
|
||||
@@ -410,8 +593,8 @@ func TestHeader_SetIntOverwrite(t *testing.T) {
|
||||
func TestHeader_SetIntNoAlloc(t *testing.T) {
|
||||
buf := make([]byte, 0, 256)
|
||||
var h Header
|
||||
h.Reset(buf)
|
||||
h.EnableBufferGrowth(false)
|
||||
h.Reset(buf, numHeaderCapacity)
|
||||
h.ConfigBufferGrowth(false)
|
||||
h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot.
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
h.SetInt("Content-Length", 1234567890, 10)
|
||||
@@ -423,3 +606,98 @@ 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), numHeaderCapacity) // 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), numHeaderCapacity)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
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 }
|
||||
|
||||
// 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() && asciiEqualFold(key, b2s(kvb.AtKey(i))) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// asciiEqualFold 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 asciiEqualFold(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)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
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, "|"))
|
||||
}
|
||||
}
|
||||
+98
-290
@@ -3,20 +3,28 @@ package httpraw
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"slices"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
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")
|
||||
// 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")
|
||||
@@ -29,6 +37,10 @@ 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
|
||||
@@ -37,38 +49,21 @@ 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.
|
||||
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.
|
||||
// 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
|
||||
}
|
||||
|
||||
type scannerState struct {
|
||||
@@ -93,60 +88,56 @@ func (h *Header) parse(asResponse bool) (err error) {
|
||||
return err
|
||||
}
|
||||
debuglog("http:firstline:done")
|
||||
err = h.parseNextHeaders()
|
||||
err = h.parseNextHeaders(h.Flags())
|
||||
debuglog("http:headers:done")
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Header) parseFirstLine(asResponse bool) (err error) {
|
||||
if len(h.hbuf.buf) > maxBufLen {
|
||||
if len(h.hbuf.kv.buf) > maxBufLen {
|
||||
return errBufferTooLarge // Offsets would overflow uint16 tokint.
|
||||
}
|
||||
flags := h.Flags()
|
||||
if asResponse {
|
||||
h.statusCode, h.statusText, h.flags, err = h.hbuf.parseFirstLineResponse(h.flags)
|
||||
h.statusCode, h.statusText, flags, err = h.hbuf.parseFirstLineResponse(flags)
|
||||
} else {
|
||||
h.method, h.requestURI, h.proto, h.flags, err = h.hbuf.parseFirstLineRequest(h.flags)
|
||||
h.method, h.requestTarget, h.proto, flags, err = h.hbuf.parseFirstLineRequest(flags)
|
||||
}
|
||||
h.hbuf.kv.flags = flags
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *Header) parseNextHeaders() error {
|
||||
func (h *Header) parseNextHeaders(flags Flags) error {
|
||||
var ss scannerState
|
||||
h.hbuf.parseNextHeaders(&ss)
|
||||
h.hbuf.parseNextHeaders(&ss, flags)
|
||||
if ss.err != nil {
|
||||
h.flags |= flagConnClose
|
||||
h.hbuf.kv.flags |= flagConnClose
|
||||
return ss.err
|
||||
}
|
||||
h.flags |= flagDoneParsingHeader
|
||||
h.hbuf.kv.flags |= flagDoneParsingHeader
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hb *headerBuf) readFromBytes(b []byte) {
|
||||
hb.buf = append(hb.buf, b...)
|
||||
}
|
||||
func (hb *headerBuf) free() int { return hb.kv.free() }
|
||||
|
||||
func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) }
|
||||
|
||||
func (hb *headerBuf) parseNextHeaders(ss *scannerState) {
|
||||
func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) {
|
||||
debuglog("http:nexthdr:loop")
|
||||
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
|
||||
for kv := hb.next(ss); kv.isValidHeader(); kv = hb.next(ss) {
|
||||
if !hb.kv.canAddOneKV() {
|
||||
ss.err = ErrHeaderTooMany
|
||||
return
|
||||
}
|
||||
hb.headers = append(hb.headers, kv)
|
||||
hb.kv.kvs = append(hb.kv.kvs, kv)
|
||||
}
|
||||
debuglog("http:nexthdr:done")
|
||||
}
|
||||
|
||||
func (hb *headerBuf) offBuf() []byte {
|
||||
return hb.buf[hb.off:]
|
||||
return hb.kv.buf[hb.off:]
|
||||
}
|
||||
|
||||
func (hb *headerBuf) skipLeadingCRLF() {
|
||||
for hb.off < len(hb.buf) && (hb.buf[hb.off] == '\n' || hb.buf[hb.off] == '\r') {
|
||||
for hb.off < len(hb.kv.buf) && (hb.kv.buf[hb.off] == '\n' || hb.kv.buf[hb.off] == '\r') {
|
||||
hb.off++
|
||||
}
|
||||
}
|
||||
@@ -156,7 +147,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.buf) {
|
||||
if hb.off < len(hb.kv.buf) {
|
||||
hb.off++ // consume newline.
|
||||
}
|
||||
return buf
|
||||
@@ -172,17 +163,17 @@ func (hb *headerBuf) scanUntilByte(c byte) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto headerSlice, flags flags, err error) {
|
||||
func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto view, 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, errNeedMore // Incomplete line.
|
||||
return method, uri, proto, flags, ErrNeedMoreData // Incomplete line.
|
||||
}
|
||||
b := hb.scanLine()
|
||||
if len(b) < 5 {
|
||||
return method, uri, proto, flags, errNeedMore
|
||||
return method, uri, proto, flags, ErrNeedMoreData
|
||||
}
|
||||
debuglog("http:req:parse")
|
||||
|
||||
@@ -190,8 +181,8 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
|
||||
reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ')
|
||||
if reqURIEnd > 0 {
|
||||
reqURIEnd += methodEnd + 1
|
||||
uri = hb.slice(b[methodEnd+1 : reqURIEnd])
|
||||
proto = hb.slice(b[reqURIEnd+1:]) // Skip space before protocol.
|
||||
uri = hb.kv.view(b[methodEnd+1 : reqURIEnd])
|
||||
proto = hb.kv.view(b[reqURIEnd+1:]) // Skip space before protocol.
|
||||
if b2s(b[reqURIEnd+1:]) != strHTTP11 {
|
||||
flags |= flagNoHTTP11
|
||||
}
|
||||
@@ -200,30 +191,30 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto
|
||||
} else {
|
||||
// No version provided.
|
||||
flags |= flagNoHTTP11
|
||||
uri = hb.slice(b[methodEnd+1:])
|
||||
uri = hb.kv.view(b[methodEnd+1:])
|
||||
}
|
||||
method = hb.slice(b[:methodEnd])
|
||||
method = hb.kv.view(b[:methodEnd])
|
||||
return method, uri, proto, flags, nil
|
||||
}
|
||||
|
||||
func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, statusText headerSlice, flags flags, err error) {
|
||||
func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, statusText view, 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, errNeedMore // Incomplete line.
|
||||
return statusCode, statusText, flags, ErrNeedMoreData // Incomplete line.
|
||||
}
|
||||
b := hb.scanLine()
|
||||
if len(b) < 5 {
|
||||
return statusCode, statusText, flags, errNeedMore
|
||||
return statusCode, statusText, flags, ErrNeedMoreData
|
||||
}
|
||||
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, errNeedMore
|
||||
return statusCode, statusText, flags, ErrNeedMoreData
|
||||
}
|
||||
if b2s(b[:protoEnd]) != strHTTP11 {
|
||||
flags |= flagNoHTTP11
|
||||
@@ -244,208 +235,27 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, status
|
||||
return statusCode, statusText, flags, errBadStatusCode
|
||||
}
|
||||
}
|
||||
statusCode = hb.slice(code)
|
||||
statusCode = hb.kv.view(code)
|
||||
if codeEnd < len(b) {
|
||||
statusText = hb.slice(b[codeEnd+1:]) // Skip space before text.
|
||||
statusText = hb.kv.view(b[codeEnd+1:]) // Skip space before text.
|
||||
}
|
||||
debuglog("http:resp:done")
|
||||
return statusCode, statusText, flags, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
func (hb *headerBuf) next(ss *scannerState) pairKV {
|
||||
if !ss.initialized {
|
||||
ss.nextColon = -1
|
||||
ss.nextNewLine = -1
|
||||
}
|
||||
buf := hb.buf[hb.off:]
|
||||
buf := hb.kv.buf[hb.off:]
|
||||
blen := len(buf)
|
||||
if blen >= 2 && buf[0] == '\r' && buf[1] == '\n' {
|
||||
hb.off += 2
|
||||
return hb.noKV() // \r\n\r\n Ends header.
|
||||
return hb.kv.noKV() // \r\n\r\n Ends header.
|
||||
} else if blen >= 1 && buf[0] == '\n' {
|
||||
hb.off += 1
|
||||
return hb.noKV() // \n\n Ends header.
|
||||
return hb.kv.noKV() // \n\n Ends header.
|
||||
}
|
||||
|
||||
// n is parsing offset. Will start by storing colon index.
|
||||
@@ -460,19 +270,19 @@ func (hb *headerBuf) next(ss *scannerState) argsKV {
|
||||
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 = errNeedMore
|
||||
return hb.noKV()
|
||||
ss.err = ErrNeedMoreData
|
||||
return hb.kv.noKV()
|
||||
} else if x < n {
|
||||
// There was a \n before the colon! This is invalid.
|
||||
ss.err = errInvalidName
|
||||
return hb.noKV()
|
||||
return hb.kv.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 errNeedMore.
|
||||
// so it still returns ErrNeedMoreData.
|
||||
ss.err = errInvalidName
|
||||
return hb.noKV()
|
||||
return hb.kv.noKV()
|
||||
}
|
||||
}
|
||||
// n stores colon position by now.
|
||||
@@ -480,12 +290,12 @@ func (hb *headerBuf) next(ss *scannerState) argsKV {
|
||||
// Spaces between the header key and colon are not allowed.
|
||||
// See RFC 7230, Section 3.2.4.
|
||||
ss.err = errInvalidName
|
||||
return hb.noKV()
|
||||
return hb.kv.noKV()
|
||||
}
|
||||
|
||||
// Ready to store key..
|
||||
var resultKV argsKV
|
||||
resultKV.key = hb.slice(buf[:n])
|
||||
var resultKV pairKV
|
||||
resultKV.key = hb.kv.view(buf[:n])
|
||||
n++ // consume colon.
|
||||
for len(buf) > n && buf[n] == ' ' {
|
||||
n++ // Trim leading spaces.
|
||||
@@ -498,8 +308,8 @@ func (hb *headerBuf) next(ss *scannerState) argsKV {
|
||||
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 = errNeedMore
|
||||
return hb.noKV()
|
||||
ss.err = ErrNeedMoreData
|
||||
return hb.kv.noKV()
|
||||
}
|
||||
n += nl + 1 // Index of the newly found newline.
|
||||
nextChar := buf[n]
|
||||
@@ -512,42 +322,40 @@ func (hb *headerBuf) next(ss *scannerState) argsKV {
|
||||
if valueEnd > valueStart && buf[valueEnd-1] == '\r' {
|
||||
valueEnd-- // Trim \r character if present before value.
|
||||
}
|
||||
resultKV.value = hb.slice(buf[valueStart:valueEnd])
|
||||
resultKV.value = hb.kv.view(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 {
|
||||
closed := h.flags.hasAny(flagConnClose) ||
|
||||
h.hasHeaderValue(headerConnection, strClose) ||
|
||||
(h.flags.hasAny(flagNoHTTP11) && !h.hasHeaderValue(headerConnection, "keep-alive"))
|
||||
flags := h.Flags()
|
||||
closed := flags.HasAny(flagConnClose) ||
|
||||
h.hasConnectionToken(strClose) ||
|
||||
(flags.HasAny(flagNoHTTP11) && !h.hasConnectionToken(strKeepAlive))
|
||||
if closed {
|
||||
h.flags |= flagConnClose
|
||||
h.hbuf.kv.flags |= flagConnClose
|
||||
}
|
||||
return closed
|
||||
}
|
||||
|
||||
// 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)),
|
||||
// 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
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const enableDebug = internal.HeapAllocDebugging
|
||||
|
||||
+75
-36
@@ -10,20 +10,17 @@ 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))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
|
||||
// Feed data in small chunks to exercise incremental parsing.
|
||||
chunks := splitInto(full, 10)
|
||||
var done bool
|
||||
var doneIdx int
|
||||
for i, chunk := range chunks {
|
||||
n, err := hdr.ReadFromBytes([]byte(chunk))
|
||||
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)
|
||||
@@ -52,19 +49,16 @@ func TestTryParse_IncrementalRequest(t *testing.T) {
|
||||
if string(hdr.Method()) != "GET" {
|
||||
t.Errorf("method = %q; want GET", hdr.Method())
|
||||
}
|
||||
if string(hdr.RequestURI()) != "/index.html" {
|
||||
t.Errorf("URI = %q; want /index.html", hdr.RequestURI())
|
||||
if string(hdr.RequestTarget()) != "/index.html" {
|
||||
t.Errorf("URI = %q; want /index.html", hdr.RequestTarget())
|
||||
}
|
||||
|
||||
// Verify headers via ForEach.
|
||||
headers := make(map[string]string)
|
||||
err := hdr.ForEach(func(key, value []byte) error {
|
||||
hdr.ForEach(func(key, value []byte) bool {
|
||||
headers[string(key)] = string(value)
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if headers["Host"] != "example.com" {
|
||||
t.Errorf("Host = %q; want example.com", headers["Host"])
|
||||
}
|
||||
@@ -85,7 +79,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))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
|
||||
chunks := splitInto(full, 8)
|
||||
var done bool
|
||||
@@ -137,7 +131,7 @@ func TestReadFromLimited(t *testing.T) {
|
||||
r := strings.NewReader(data)
|
||||
|
||||
var hdr Header
|
||||
hdr.Reset(make([]byte, 0, 256))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
|
||||
// Read in one shot.
|
||||
n, err := hdr.ReadFromLimited(r, 256)
|
||||
@@ -163,7 +157,7 @@ func TestReadFromLimited(t *testing.T) {
|
||||
|
||||
func TestReadFromLimited_MaxBytes(t *testing.T) {
|
||||
var hdr Header
|
||||
hdr.Reset(make([]byte, 0, 256))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
|
||||
// Zero maxBytesToRead should error.
|
||||
_, err := hdr.ReadFromLimited(strings.NewReader("data"), 0)
|
||||
@@ -174,9 +168,9 @@ func TestReadFromLimited_MaxBytes(t *testing.T) {
|
||||
|
||||
func TestReadFromBytes_Empty(t *testing.T) {
|
||||
var hdr Header
|
||||
hdr.Reset(make([]byte, 0, 256))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
|
||||
_, err := hdr.ReadFromBytes(nil)
|
||||
err := hdr.ReadFromBytes(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty bytes")
|
||||
}
|
||||
@@ -184,7 +178,7 @@ func TestReadFromBytes_Empty(t *testing.T) {
|
||||
|
||||
func TestBufferFreeAndCapacity(t *testing.T) {
|
||||
var hdr Header
|
||||
hdr.Reset(make([]byte, 0, 100))
|
||||
hdr.Reset(make([]byte, 0, 100), numHeaderCapacity)
|
||||
|
||||
if hdr.BufferCapacity() != 100 {
|
||||
t.Errorf("capacity = %d; want 100", hdr.BufferCapacity())
|
||||
@@ -202,15 +196,14 @@ func TestBufferFreeAndCapacity(t *testing.T) {
|
||||
func TestEnableBufferGrowth(t *testing.T) {
|
||||
var hdr Header
|
||||
buf := make([]byte, 0, 64)
|
||||
hdr.Reset(buf)
|
||||
hdr.EnableBufferGrowth(false)
|
||||
|
||||
hdr.Reset(buf, numHeaderCapacity)
|
||||
hdr.ConfigBufferGrowth(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")
|
||||
}
|
||||
@@ -229,11 +222,11 @@ func TestHeader_Add(t *testing.T) {
|
||||
|
||||
// ForEach should find both.
|
||||
var values []string
|
||||
hdr.ForEach(func(key, value []byte) error {
|
||||
hdr.ForEach(func(key, value []byte) bool {
|
||||
if string(key) == "X-Custom" {
|
||||
values = append(values, string(value))
|
||||
}
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
if len(values) != 2 {
|
||||
t.Fatalf("expected 2 X-Custom headers, got %d", len(values))
|
||||
@@ -332,6 +325,14 @@ 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")
|
||||
}
|
||||
@@ -357,13 +358,10 @@ func TestCookie_ForEach(t *testing.T) {
|
||||
c.ParseBytes([]byte("a=1; b=2; c=3"))
|
||||
|
||||
var keys []string
|
||||
err := c.ForEach(func(key, value []byte) error {
|
||||
c.ForEach(func(key, value []byte) bool {
|
||||
keys = append(keys, string(key))
|
||||
return nil
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(keys) != 3 {
|
||||
t.Fatalf("expected 3 cookie entries, got %d", len(keys))
|
||||
}
|
||||
@@ -389,7 +387,7 @@ func TestHeader_MultilineValue(t *testing.T) {
|
||||
|
||||
func TestHeader_ResponseRoundTrip(t *testing.T) {
|
||||
var hdr Header
|
||||
hdr.Reset(make([]byte, 0, 256))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
hdr.SetProtocol("HTTP/1.1")
|
||||
hdr.SetStatus("404", "Not Found")
|
||||
hdr.Add("Content-Type", "text/plain")
|
||||
@@ -429,10 +427,10 @@ func TestHeader_ResponseRoundTrip(t *testing.T) {
|
||||
|
||||
func TestHeader_RequestRoundTrip(t *testing.T) {
|
||||
var hdr Header
|
||||
hdr.Reset(make([]byte, 0, 256))
|
||||
hdr.Reset(make([]byte, 0, 256), numHeaderCapacity)
|
||||
hdr.SetProtocol("HTTP/1.1")
|
||||
hdr.SetMethod("POST")
|
||||
hdr.SetRequestURI("/api/data")
|
||||
hdr.SetRequestTarget("/api/data")
|
||||
hdr.Add("Host", "example.com")
|
||||
hdr.Add("Content-Type", "application/json")
|
||||
|
||||
@@ -454,8 +452,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.RequestURI()) != "/api/data" {
|
||||
t.Errorf("re-parsed URI = %q; want /api/data", hdr2.RequestURI())
|
||||
if string(hdr2.RequestTarget()) != "/api/data" {
|
||||
t.Errorf("re-parsed URI = %q; want /api/data", hdr2.RequestTarget())
|
||||
}
|
||||
if string(hdr2.Get("Host")) != "example.com" {
|
||||
t.Errorf("re-parsed Host = %q; want example.com", hdr2.Get("Host"))
|
||||
@@ -504,8 +502,8 @@ func TestParseRequest_NoProtocol(t *testing.T) {
|
||||
if string(hdr.Method()) != "GET" {
|
||||
t.Errorf("method = %q; want GET", hdr.Method())
|
||||
}
|
||||
if string(hdr.RequestURI()) != "/simple" {
|
||||
t.Errorf("URI = %q; want /simple", hdr.RequestURI())
|
||||
if string(hdr.RequestTarget()) != "/simple" {
|
||||
t.Errorf("URI = %q; want /simple", hdr.RequestTarget())
|
||||
}
|
||||
if hdr.Protocol() != nil {
|
||||
t.Errorf("protocol should be nil for version-less request, got %q", hdr.Protocol())
|
||||
@@ -539,3 +537,44 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user