httpraw looking good

This commit is contained in:
soypat
2025-05-25 16:58:52 -03:00
parent b63ce5a964
commit ca7bf2f366
9 changed files with 1080 additions and 2003 deletions
+165
View File
@@ -0,0 +1,165 @@
package httpraw
import (
"bytes"
"errors"
)
// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [Header] methods.
type Cookie struct {
buf []byte
kvs []argsKV // first key-value pair is the data Key/Value pair.
}
// Reset functions very similarly to [Header.Reset]. Can be used for in-place cookie parsing.
func (c *Cookie) Reset(buf []byte) {
if buf == nil {
buf = c.buf[:0]
}
*c = Cookie{
buf: buf,
kvs: c.kvs[:0],
}
}
func (c *Cookie) Key() []byte {
if len(c.kvs) == 0 || c.kvs[0].key.len == 0 {
return nil
}
return tok2bytes(c.buf, c.kvs[0].key)
}
func (c *Cookie) Value() []byte {
if len(c.kvs) == 0 || c.kvs[0].value.len == 0 {
return nil
}
return tok2bytes(c.buf, c.kvs[0].value)
}
func (c *Cookie) ParseBytes(cookie []byte) error {
c.Reset(nil)
c.buf = append(c.buf[:0], cookie...)
return c.Parse()
}
func (c *Cookie) CopyTo(dst *Cookie) {
dst.buf = append(dst.buf[:0], c.buf...)
dst.kvs = append(dst.kvs[:0], c.kvs...)
}
func (c *Cookie) Parse() error {
if len(c.kvs) > 0 {
return errors.New("cookies already parsed, reset before parsing again")
}
off := 0
for {
k, v, n := parseCookie(c.buf[off:])
if n == 0 {
break
}
c.kvs = append(c.kvs, argsKV{
key: bytes2tok(c.buf, k),
value: bytes2tok(c.buf, v),
})
off += n
}
return nil
}
func (c *Cookie) ForEach(cb func(key, value []byte) error) error {
nc := len(c.kvs)
for i := 0; i < nc; i++ {
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
}
func (c *Cookie) Get(key string) []byte {
nc := len(c.kvs)
for i := 0; i < nc; i++ {
kv := c.kvs[i]
if b2s(tok2bytes(c.buf, kv.key)) == key {
return tok2bytes(c.buf, kv.value)
}
}
return nil
}
func (c *Cookie) HasValueOrKey(keyOrSingleValue string) bool {
nc := len(c.kvs)
for i := 0; i < nc; i++ {
kv := c.kvs[i]
if kv.key.len == 0 && b2s(tok2bytes(c.buf, kv.value)) == keyOrSingleValue ||
b2s(tok2bytes(c.buf, kv.key)) == keyOrSingleValue {
return true
}
}
return false
}
// parseCookie parses a cookie inside cookie buffer and adds it to cookie buffer..
//
// Cookie: <cookie>\r\n
func parseCookie(cookie []byte) (key, value []byte, cookieEnd int) {
if len(cookie) == 0 {
return nil, nil, 0
}
valueEnd := bytes.IndexByte(cookie, ';')
if valueEnd < 0 { // Ouch this `if` looks like it kills CPU pipepline.
valueEnd = len(cookie)
cookieEnd = len(cookie)
} else {
cookieEnd = valueEnd + 1
}
eqIdx := bytes.IndexByte(cookie[:valueEnd], '=')
key = cookie[:0]
if eqIdx > 0 {
key = trimCookie(cookie[:eqIdx], false)
}
value = trimCookie(cookie[eqIdx+1:valueEnd], true)
return key, value, cookieEnd
}
func trimCookie(src []byte, trimQuotes bool) []byte {
for len(src) > 0 && src[0] == ' ' {
src = src[1:] // skip leading whitespace.
}
for len(src) > 0 && src[len(src)-1] == ' ' {
src = src[:len(src)-1] // skip trailing whitespace
}
if trimQuotes {
if len(src) > 1 && src[0] == '"' && src[len(src)-1] == '"' {
src = src[1 : len(src)-1] // Trim leading+trailing quotes.
}
}
return src
}
func (c *Cookie) String() string {
buf := c.AppendKeyValues(nil)
return b2s(buf)
}
func (c *Cookie) AppendKeyValues(dst []byte) []byte {
nc := len(c.kvs)
for i := 0; i < nc; i++ {
kv := c.kvs[i]
key := tok2bytes(c.buf, kv.key)
value := tok2bytes(c.buf, kv.value)
if len(key) != 0 {
dst = append(dst, key...)
dst = append(dst, '=')
}
dst = append(dst, value...)
if i+1 < nc {
dst = append(dst, ';', ' ')
}
}
return dst
}
+358
View File
@@ -0,0 +1,358 @@
package httpraw
import (
"errors"
"io"
"net/http"
"slices"
)
const (
strHTTP11 = "HTTP/1.1"
strCRLF = "\r\n"
headerCookie = "Cookie"
headerConnection = "Connection"
strClose = "close"
)
type flags uint16
const (
flagNoBufferGrow flags = 1 << iota
flagDoneParsingHeader
flagOOMReached
flagConnClose
flagNoHTTP11
flagMangledBuffer // set when header fields appended to buffer via Add,Set calls
)
func (f flags) hasAny(checkThese flags) bool {
return f&checkThese != 0
}
// Header implements "raw" HTTP validation and header key-value parsing, validation and marshalling.
//
// It does NOT implement:
// - Normalization.
// - Cookies.
// - Special header optimizations.
// - Safe API. Users can easily mangle HTTP body with calls.
type Header struct {
hbuf headerBuf
// Request fields.
method headerSlice
requestURI headerSlice
proto headerSlice
// Response fields.
statusCode headerSlice
statusText headerSlice
flags flags
_ noCopy
}
// EnableBufferGrow disables buffer growth during parsing if b is false. Is enabled by default.
func (h *Header) EnableBufferGrow(b bool) {
if !b {
h.flags |= flagNoBufferGrow
} else {
h.flags &^= flagNoBufferGrow
}
}
// ParseBytes copies the bytes into buffer and parses the HTTP header. It fails if HTTP header data is incomplete.
func (h *Header) ParseBytes(asResponse bool, b []byte) error {
h.Reset(nil)
h.hbuf.readFromBytes(b)
return h.parse(asResponse)
}
// Parse parses accumulated data in-place with no copying. One can set HTTP header data buffer with [Header.Reset].
// It fails if HTTP data is incomplete.
func (h *Header) Parse(asResponse bool) error {
h.Reset(h.hbuf.buf)
return h.parse(asResponse)
}
// TryParse begins parsing or resumes parsing from a failed previous attempt from any of the Parse* methods.
// It fails if HTTP data is incomplete. It panics if called after header parsing completed succesfully.
// As long as ok returns true future calls to TryParse may succeed.
//
// ok, err := h.TryParse()
// for ; ok; ok, err = h.TryParse() {
// _, err = h.ReadFrom(r, 256)
// if err != nil && err != io.EOF {
// return err
// }
// }
// if err != nil {
// return err
// }
func (h *Header) TryParse(asResponse bool) (ok bool, err error) {
if h.flags.hasAny(flagDoneParsingHeader) {
return false, errors.New("TryParse called after header parsed")
} else if h.flags.hasAny(flagMangledBuffer) {
return false, errMangledBuffer
}
if asResponse && h.statusCode.len == 0 || !asResponse && h.requestURI.start == 0 {
err = h.parseFirstLine(asResponse)
if err != nil {
return err == errNeedMore, err
}
}
err = h.parseNextHeaders()
return err == nil || err == errNeedMore, err
}
// ReadFromLimited reads at most maxBytesToRead from reader and appends them to underlying buffer.
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
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.Free()
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)
h.hbuf.buf = h.hbuf.buf[:blen+n]
return n, err
}
// ReadFromBytes appends argument buffer to underlying buffer.
// Used to accumulate HTTP header for later parsing with [Header.TryParse].
func (h *Header) ReadFromBytes(b []byte) (int, error) {
if len(b) == 0 {
return 0, errSmallBuffer
}
free := h.Free()
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
}
// Free returns amount of bytes free in underlying buffer.
func (h *Header) Free() int {
return h.hbuf.free()
}
// 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 := 0; i < nh; i++ {
kv := hb.headers[i]
if !kv.isValid() {
continue
}
key := hb.musttoken(kv.key)
value := hb.musttoken(kv.value)
err := cb(key, value)
if err != nil {
return err
}
}
return nil
}
// Reset discards all parsed data and sets the buffer data to buf. This method
// can be used to avoid copying and growing buffers. Call [Header.Parse] after setting buffer
// 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(nil) // Reuse buffer previously set in a call to Reset.
func (h *Header) Reset(buf []byte) {
if h.flags.hasAny(flagNoBufferGrow) && len(buf) < 32 {
panic("small buffer and flagNoBufferGrow set")
}
const persistentFlags = flagNoBufferGrow
h.hbuf.reset(buf)
*h = Header{
hbuf: h.hbuf,
flags: h.flags & persistentFlags,
}
}
// 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) {
if h.flags.hasAny(flagMangledBuffer) {
return nil, errMangledBuffer
} else if h.flags.hasAny(flagDoneParsingHeader) {
return h.hbuf.buf[h.hbuf.off:], nil
}
return nil, errUnparsed
}
// Set sets a key-value pair in the HTTP header. It mangles the buffer.
func (h *Header) Set(key, value string) {
kv := h.peekPtrHeader(key)
if kv != nil {
kv.invalidate()
}
h.appendHeader(key, value)
}
func (h *Header) Get(key string) []byte {
kv := h.peekHeader(key)
if kv.isValid() {
return h.hbuf.musttoken(kv.value)
}
return nil
}
func (h *Header) Add(key, value string) {
h.appendHeader(key, value)
}
// Method returns HTTP request method.
func (h *Header) Method() []byte {
return h.getNonEmptyValue(h.method)
}
// SetRequestURI sets RequestURI for the first HTTP request line.
func (h *Header) SetRequestURI(requestURI string) {
h.requestURI = h.reuseOrAppend(h.requestURI, requestURI)
}
// RequestURI returns RequestURI from the first HTTP request line.
func (h *Header) RequestURI() []byte {
return h.getNonEmptyValue(h.requestURI)
}
func (h *Header) SetMethod(method string) {
h.method = h.reuseOrAppend(h.method, method)
}
// Protocol returns HTTP protocol.
func (h *Header) Protocol() []byte {
return h.getNonEmptyValue(h.proto)
}
func (h *Header) SetProtocol(protocol string) {
h.proto = h.reuseOrAppend(h.proto, protocol)
}
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)
}
func (h *Header) SetStatus(code, statusText string) {
h.statusCode = h.reuseOrAppend(h.statusCode, code)
h.statusText = h.reuseOrAppend(h.statusText, statusText)
}
func (h *Header) getNonEmptyValue(s headerSlice) []byte {
if s.len == 0 {
return nil // If empty then value is invalid, return nil.
}
return h.hbuf.musttoken(s)
}
// AppendRequest appends the request representation to the buffer and returns the result.
func (h *Header) AppendRequest(dst []byte) ([]byte, error) {
if h.flags.hasAny(flagOOMReached) {
return dst, errOOM
} else if h.requestURI.len == 0 || h.proto.len == 0 || h.method.len == 0 {
return dst, errors.New("need method/protocol/request URI to create request header")
}
method := h.Method()
if len(method) == 0 {
dst = append(dst, http.MethodGet...)
} else {
dst = append(dst, method...)
}
uri := h.RequestURI()
proto := h.Protocol()
dst = append(dst, ' ')
dst = append(dst, uri...)
dst = append(dst, ' ')
dst = append(dst, proto...)
dst = append(dst, strCRLF...)
dst = h.AppendHeaders(dst)
return append(dst, strCRLF...), nil
}
// AppendResponse appends the response representation to the buffer and returns the result.
func (h *Header) AppendResponse(dst []byte) ([]byte, error) {
if h.flags.hasAny(flagOOMReached) {
return dst, errOOM
} else if h.statusCode.len == 0 || h.statusText.len == 0 {
return dst, errors.New("invalid status code or text")
}
code, text := h.Status()
dst = append(dst, code...)
dst = append(dst, ' ')
dst = append(dst, text...)
dst = append(dst, strCRLF...)
dst = h.AppendHeaders(dst)
return append(dst, strCRLF...), nil
}
// AppendHeaders appends headers to buffer. Use AppendRequest and AppendResponse over this.
// Does not append extra \r\n to end. Appends nothing if contains no headers.
func (h *Header) AppendHeaders(dst []byte) []byte {
for i, n := 0, len(h.hbuf.headers); i < n; i++ {
kv := &h.hbuf.headers[i]
if kv.isValid() {
key := h.hbuf.musttoken(kv.key)
value := h.hbuf.musttoken(kv.value)
dst = appendHeaderLine(dst, b2s(key), b2s(value))
}
}
return dst
}
func (h *Header) String() string {
buf, err := h.AppendRequest(nil)
if err != nil {
buf, err = h.AppendResponse(nil)
if err != nil {
return err.Error()
}
}
return b2s(buf)
}
func appendHeaderLine(dst []byte, key, value string) []byte {
dst = append(dst, key...)
dst = append(dst, ':', ' ')
dst = append(dst, value...)
return append(dst, strCRLF...)
}
// Embed this type into a struct, which mustn't be copied,
// so `go vet` gives a warning if this struct is copied.
//
// See https://github.com/golang/go/issues/8005#issuecomment-190753527 for details.
// and also: https://stackoverflow.com/questions/52494458/nocopy-minimal-example
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
+146
View File
@@ -0,0 +1,146 @@
package httpraw
import (
"bytes"
"fmt"
"net/http"
"strconv"
"strings"
"testing"
"time"
)
func TestHeaderParseRequest(t *testing.T) {
const (
wantMethod = "GET"
wantURI = "/data/set"
wantMessage = "hello world!"
asRequest = false
asResponse = true
)
req, err := http.NewRequest(wantMethod, wantURI, strings.NewReader(wantMessage))
if err != nil {
t.Fatal(err)
}
var wantCookie http.Cookie
wantCookie.SameSite = http.SameSiteLaxMode
wantCookie.MaxAge = 360000
wantCookie.Name = "key"
wantCookie.Value = "value"
wantCookie.Expires = time.Now().Add(time.Hour)
wantCookie.Domain = "DOM"
wantCookie.HttpOnly = true
wantCookie.Secure = true
wantCookie.Path = "/abc"
req.Header.Set("Cookie", wantCookie.String())
t.Log("valid cookie:", wantCookie.Valid() == nil, wantCookie.String())
var buf bytes.Buffer
req.Write(&buf)
var hdr Header
msg := buf.Bytes()
start := time.Now()
err = hdr.ParseBytes(asRequest, msg)
elapsed := time.Since(start)
if err != nil {
t.Fatal(err)
}
fmt.Printf("%s\nparsed in %s\n\n", msg, elapsed.String())
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())
}
contentLength, _ := strconv.Atoi(string(hdr.Get("Content-Length")))
if contentLength != len(wantMessage) {
t.Errorf("want Content-Length %d, got %d", len(wantMessage), contentLength)
}
var c Cookie
cookie := hdr.Get("Cookie")
c.Reset(cookie)
err = c.Parse()
if err != nil {
t.Error(err)
}
key := string(c.Key())
if key != wantCookie.Name {
t.Errorf("want cookie key %q, got %q", wantCookie.Name, key)
}
value := string(c.Value())
if value != wantCookie.Value {
t.Errorf("want cookie key %q, got %q", wantCookie.Value, value)
}
domain := string(c.Get("Domain"))
if domain != wantCookie.Domain {
t.Errorf("want domain %q, got %q", wantCookie.Domain, domain)
}
httpOnly := c.HasValueOrKey("HttpOnly")
if httpOnly != wantCookie.HttpOnly {
t.Errorf("want cookie HttpOnly %v, got %v", wantCookie.HttpOnly, httpOnly)
}
secure := c.HasValueOrKey("Secure")
if secure != wantCookie.Secure {
t.Errorf("want cookie HttpOnly %v, got %v", wantCookie.Secure, secure)
}
samesite := string(c.Get("SameSite"))
if samesite != strSameSite(wantCookie.SameSite) {
t.Errorf("want cookie SameSite %v, got %v", strSameSite(wantCookie.SameSite), samesite)
}
body, err := hdr.Body()
if err != nil {
t.Error(err)
}
if string(body) != wantMessage {
t.Errorf("want body message %q, got %q", wantMessage, body)
}
cookieStr := string(c.AppendKeyValues(nil))
if wantCookie.String() != cookieStr {
t.Errorf("want full cookie representation\n%qgot:\n%q", wantCookie.String(), cookieStr)
}
data, _ := hdr.AppendRequest(nil)
fmt.Printf("%s", data)
}
func BenchmarkParseBytes(b *testing.B) {
b.StopTimer()
const (
wantMethod = "GET"
wantURI = "/data/set"
wantMessage = "hello world!"
asRequest = false
)
req, _ := http.NewRequest(wantMethod, wantURI, strings.NewReader(wantMessage))
var buf bytes.Buffer
req.Write(&buf)
data := buf.Bytes()
b.StartTimer()
for i := 0; i < b.N; i++ {
var hdr Header
err := hdr.ParseBytes(asRequest, data)
if err != nil {
b.Fatal(err)
}
_, err = hdr.Body()
if err != nil {
b.Fatal(err)
}
}
}
func strSameSite(mode http.SameSite) string {
switch mode {
case http.SameSiteLaxMode:
return "Lax"
case http.SameSiteDefaultMode:
return ""
case http.SameSiteStrictMode:
return "Strict"
case http.SameSiteNoneMode:
return "None"
default:
panic("invalid same site")
}
}
+411
View File
@@ -0,0 +1,411 @@
package httpraw
import (
"bytes"
"errors"
"slices"
"unsafe"
)
var (
errNeedMore = errors.New("need more data: cannot find trailing lf")
errUnparsed = errors.New("need to finish parsing")
errInvalidName = errors.New("invalid header name")
errSmallBuffer = errors.New("small read buffer. Increase ReadBufferSize")
errOOM = errors.New("httpraw: buffer out of memory")
// Header.Set and Header.Add mangles the buffer.
// Call them after retrieving the Body. Do not call them before parsing the header (why would you even do that?).
errMangledBuffer = errors.New("httpraw: mangled buffer")
errNoCookies = errors.New("no cookie found")
)
type headerBuf struct {
// 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
// offset into buf for parsing.
off int
// args contains key-value store.
headers []argsKV
}
type tokint = uint16
type headerSlice struct {
start tokint
len tokint
}
type argsKV struct {
key headerSlice
value headerSlice // value start >0 means value is present.
}
type scannerState struct {
err error
// by checking whether the next line contains a colon or not to tell
// it's a header entry or a multi line value of current header entry.
// the side effect of this operation is that we know the index of the
// next colon and new line, so this can be used during next iteration,
// instead of find them again.
nextColon int
nextNewLine int
initialized bool
}
func (h *Header) parse(asResponse bool) (err error) {
err = h.parseFirstLine(asResponse)
if err != nil {
return err
}
return h.parseNextHeaders()
}
func (h *Header) parseFirstLine(asResponse bool) (err error) {
if asResponse {
h.statusCode, h.statusText, h.flags, err = h.hbuf.parseFirstLineResponse(h.flags)
} else {
h.method, h.requestURI, h.proto, h.flags, err = h.hbuf.parseFirstLineRequest(h.flags)
}
return err
}
func (h *Header) parseNextHeaders() error {
var ss scannerState
h.hbuf.parseNextHeaders(&ss)
if ss.err != nil {
h.flags |= flagConnClose
return ss.err
}
h.flags |= flagDoneParsingHeader
return nil
}
func (hb *headerBuf) readFromBytes(b []byte) {
hb.buf = append(hb.buf, b...)
}
func (hb *headerBuf) free() int { return cap(hb.buf) - len(hb.buf) }
func (hb *headerBuf) parseNextHeaders(ss *scannerState) {
for kv := hb.next(ss); kv.isValid(); kv = hb.next(ss) {
hb.headers = append(hb.headers, kv)
}
}
func (hb *headerBuf) offBuf() []byte {
return hb.buf[hb.off:]
}
func (hb *headerBuf) scanLine() []byte {
buf := hb.scanUntilByte('\n')
if len(buf) > 0 && buf[len(buf)-1] == '\r' {
buf = buf[:len(buf)-1] // exclude carriage return.
}
if hb.off < len(hb.buf) {
hb.off++ // consume newline.
}
return buf
}
func (hb *headerBuf) scanUntilByte(c byte) []byte {
buf := hb.offBuf()
idx := bytes.IndexByte(buf, c)
if idx >= 0 {
buf = buf[:idx]
}
hb.off += len(buf)
return buf
}
func (hb *headerBuf) parseFirstLineRequest(initFlags flags) (method, uri, proto headerSlice, flags flags, err error) {
hb.off = 0 // Parsing first line resets offset.
var b []byte
for len(b) == 0 {
b = hb.scanLine()
}
flags = initFlags
if len(b) < 5 {
return method, uri, proto, flags, errNeedMore
}
methodEnd := max(0, bytes.IndexByte(b, ' '))
reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ')
if reqURIEnd > 0 {
reqURIEnd += methodEnd + 1
uri = hb.slice(b[methodEnd+1 : reqURIEnd])
if b2s(b[methodEnd+1:reqURIEnd]) != strHTTP11 {
flags |= flagNoHTTP11
}
} else if reqURIEnd == 0 {
return method, uri, proto, flags, errors.New("empty URI")
} else {
// No version provided.
reqURIEnd = methodEnd + 1
flags |= flagNoHTTP11
uri = hb.slice(b[methodEnd+1 : reqURIEnd])
}
proto = hb.slice(b[reqURIEnd:])
method = hb.slice(b[:methodEnd])
return method, uri, proto, flags, nil
}
func (hb *headerBuf) parseFirstLineResponse(initFlags flags) (statusCode, statusText headerSlice, flags flags, err error) {
hb.off = 0 // Parsing first line resets offset.
var b []byte
for len(b) == 0 {
b = hb.scanLine()
}
flags = initFlags
if len(b) < 5 {
return statusCode, statusText, flags, errNeedMore
}
statusCodeEnd := max(0, bytes.IndexByte(b, ' '))
if statusCodeEnd < 0 {
return statusCode, statusText, flags, errors.New("missing status code")
}
code := b[:statusCodeEnd]
text := b[statusCodeEnd:]
if len(code) > 3 {
return statusCode, statusText, flags, errors.New("long status code")
}
for i := range code {
if code[i] > '9' || code[i] < '0' {
return statusCode, statusText, flags, errors.New("invalid status code")
}
}
statusCode = hb.slice(code)
statusText = hb.slice(text)
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 (h *Header) peekPtrHeader(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 nil
}
func (hb *headerBuf) mustAppendSlice(value string) headerSlice {
L := len(hb.buf)
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 {
free := h.hbuf.free()
if len(value) > free {
if h.flags.hasAny(flagNoBufferGrow) {
h.flags |= flagOOMReached
return headerSlice{}
}
h.hbuf.buf = slices.Grow(h.hbuf.buf, len(value))
}
h.flags |= flagMangledBuffer
return h.hbuf.mustAppendSlice(value)
}
func (h *Header) appendHeader(key, value string) {
hb := &h.hbuf
free := hb.free()
buf := h.hbuf.buf
if len(key)+len(value) > free {
if h.flags.hasAny(flagNoBufferGrow) {
panic(errSmallBuffer)
}
hb.buf = slices.Grow(buf, len(key)+len(value))
}
h.flags |= flagMangledBuffer
k := hb.mustAppendSlice(key)
v := hb.mustAppendSlice(value)
hb.headers = append(hb.headers, argsKV{
key: k,
value: v,
})
}
func (hb *headerBuf) noKV() argsKV { return argsKV{} }
func (hb *headerBuf) next(ss *scannerState) argsKV {
if !ss.initialized {
ss.nextColon = -1
ss.nextNewLine = -1
}
buf := hb.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.
} else if blen >= 1 && buf[0] == '\n' {
hb.off += 1
return hb.noKV() // \n\n Ends header.
}
// n is parsing offset. Will start by storing colon index.
n := 0
if ss.nextColon >= 0 {
// Retake from last colon found.
n = ss.nextColon
ss.nextColon = -1
} else {
n = bytes.IndexByte(buf, ':')
x := bytes.IndexByte(buf, '\n')
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()
} else if x < n {
// There was a \n before the colon! This is invalid.
ss.err = errInvalidName
return hb.noKV()
} else if n < 0 {
// No colon found, probably missing data.
ss.err = errNeedMore
return hb.noKV()
}
}
// n stores colon position by now.
if bytes.IndexByte(buf[:n], ' ') >= 0 || bytes.IndexByte(buf[:n], '\t') >= 0 {
// Spaces between the header key and colon are not allowed.
// See RFC 7230, Section 3.2.4.
ss.err = errInvalidName
return hb.noKV()
}
// Ready to store key..
var resultKV argsKV
resultKV.key = hb.slice(buf[:n])
n++ // consume colon.
for len(buf) > n && buf[n] == ' ' {
n++ // Trim leading spaces.
}
// n now points to start of value.
valueStart := n
// Find end of value. Values may be multiline, in which case we must treat newlines followed by whitespace as part of the value.
for {
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()
}
n += nl + 1 // Index of the newly found newline.
nextChar := buf[n]
if nextChar != ' ' && nextChar != '\t' {
break // End of value found.
}
}
valueEnd := n - 1 // Trim newline.
if valueEnd > valueStart && buf[valueEnd-1] == '\r' {
valueEnd-- // Trim \r character if present before value.
}
resultKV.value = hb.slice(buf[valueStart:valueEnd])
hb.off += n
return resultKV
}
// 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.
}
*h = headerBuf{
buf: buf,
headers: h.headers[:0],
}
}
// 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.flags.hasAny(flagNoHTTP11) && !h.hasHeaderValue("Connection", "keep-alive"))
if closed {
h.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))
}
// s2b converts string to a byte slice without memory allocation.
func s2b(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
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)),
}
}