mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
httpraw looking good
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user