diff --git a/README.md b/README.md index b14a2e0..6265824 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Userspace networking primitives. ## Packages - `lneto`: Low-level Networking Operations, or "El Neto", the networking package. Zero copy network frame marshalling and unmarshalling. - [`lneto/frames.go`](./frames.go): Ethernet, IPv4/IPv6, ARP, TCP, UDP packet marshalling/unmarshalling. - +- [`lneto/http/httpraw`](./http/httpraw/): Heapless HTTP header processing and validation. Does no implement header normalization. - [`lneto/tcp`](./ntp): TCP implementation and low level logic. - [`lneto/dhcpv4`](./dhcpv4): DHCP version 4 protocol implementation and low level logic. - [`lneto/dns`](./dns): DNS protocol implementation and low level logic. diff --git a/go.mod b/go.mod index d8dd8d7..1e4a7d1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/soypat/lneto -go 1.21 +go 1.23.8 diff --git a/http/httpraw/cookie.go b/http/httpraw/cookie.go new file mode 100644 index 0000000..d16b746 --- /dev/null +++ b/http/httpraw/cookie.go @@ -0,0 +1,168 @@ +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 + } + if len(c.kvs) == 0 { + return errNoCookies + } + 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: \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 +} diff --git a/http/httpraw/header.go b/http/httpraw/header.go new file mode 100644 index 0000000..392da47 --- /dev/null +++ b/http/httpraw/header.go @@ -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 (see [Cookie]). +// - 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() {} diff --git a/http/httpraw/header_test.go b/http/httpraw/header_test.go new file mode 100644 index 0000000..c00ba22 --- /dev/null +++ b/http/httpraw/header_test.go @@ -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") + } +} diff --git a/http/httpraw/parse.go b/http/httpraw/parse.go new file mode 100644 index 0000000..3116512 --- /dev/null +++ b/http/httpraw/parse.go @@ -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)), + } +}