diff --git a/http/httpraw/cookie.go b/http/httpraw/cookie.go new file mode 100644 index 0000000..091d3b0 --- /dev/null +++ b/http/httpraw/cookie.go @@ -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: \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..a174c19 --- /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. +// - 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)), + } +} diff --git a/httpx/cookie.go b/httpx/cookie.go deleted file mode 100644 index 611af48..0000000 --- a/httpx/cookie.go +++ /dev/null @@ -1,700 +0,0 @@ -package httpx - -import ( - "bytes" - "errors" - "io" - "path/filepath" - "strconv" - "strings" - "sync" - "time" -) - -var zeroTime time.Time - -var ( - // cookieExpireDelete may be set on Cookie.Expire for expiring the given cookie. - cookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) - - // cookieExpireUnlimited indicates that the cookie doesn't expire. - cookieExpireUnlimited = zeroTime -) - -// CookieSameSite is an enum for the mode in which the SameSite flag should be set for the given cookie. -// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details. -type CookieSameSite int - -const ( - // CookieSameSiteDisabled removes the SameSite flag. - CookieSameSiteDisabled CookieSameSite = iota - // CookieSameSiteDefaultMode sets the SameSite flag. - CookieSameSiteDefaultMode - // CookieSameSiteLaxMode sets the SameSite flag with the "Lax" parameter. - CookieSameSiteLaxMode - // CookieSameSiteStrictMode sets the SameSite flag with the "Strict" parameter. - CookieSameSiteStrictMode - // CookieSameSiteNoneMode sets the SameSite flag with the "None" parameter. - // See https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 - CookieSameSiteNoneMode -) - -// acquireCookie returns an empty Cookie object from the pool. -// -// The returned object may be returned back to the pool with ReleaseCookie. -// This allows reducing GC load. -func acquireCookie() *Cookie { - return cookiePool.Get().(*Cookie) -} - -// releaseCookie returns the Cookie object acquired with AcquireCookie back -// to the pool. -// -// Do not access released Cookie object, otherwise data races may occur. -func releaseCookie(c *Cookie) { - c.Reset() - cookiePool.Put(c) -} - -var cookiePool = &sync.Pool{ - New: func() any { - return &Cookie{} - }, -} - -// Cookie represents HTTP response cookie. -// -// Do not copy Cookie objects. Create new object and use CopyTo instead. -// -// Cookie instance MUST NOT be used from concurrently running goroutines. -type Cookie struct { - noCopy noCopy - - key []byte - value []byte - expire time.Time - maxAge int - domain []byte - path []byte - - httpOnly bool - secure bool - sameSite CookieSameSite - - bufKV argsKV - buf []byte -} - -// CopyTo copies src cookie to c. -func (c *Cookie) CopyTo(src *Cookie) { - c.Reset() - c.key = append(c.key, src.key...) - c.value = append(c.value, src.value...) - c.expire = src.expire - c.maxAge = src.maxAge - c.domain = append(c.domain, src.domain...) - c.path = append(c.path, src.path...) - c.httpOnly = src.httpOnly - c.secure = src.secure - c.sameSite = src.sameSite -} - -// HTTPOnly returns true if the cookie is http only. -func (c *Cookie) HTTPOnly() bool { - return c.httpOnly -} - -// SetHTTPOnly sets cookie's httpOnly flag to the given value. -func (c *Cookie) SetHTTPOnly(httpOnly bool) { - c.httpOnly = httpOnly -} - -// Secure returns true if the cookie is secure. -func (c *Cookie) Secure() bool { - return c.secure -} - -// SetSecure sets cookie's secure flag to the given value. -func (c *Cookie) SetSecure(secure bool) { - c.secure = secure -} - -// SameSite returns the SameSite mode. -func (c *Cookie) SameSite() CookieSameSite { - return c.sameSite -} - -// SetSameSite sets the cookie's SameSite flag to the given value. -// Set value CookieSameSiteNoneMode will set Secure to true also to avoid browser rejection. -func (c *Cookie) SetSameSite(mode CookieSameSite) { - c.sameSite = mode - if mode == CookieSameSiteNoneMode { - c.SetSecure(true) - } -} - -// Path returns cookie path. -func (c *Cookie) Path() []byte { - return c.path -} - -// SetPath sets cookie path. -func (c *Cookie) SetPath(path string) { - c.buf = append(c.buf[:0], path...) - c.path = normalizePath(c.path, b2s(c.buf)) -} - -// SetPathBytes sets cookie path. -func (c *Cookie) SetPathBytes(path []byte) { - c.buf = append(c.buf[:0], path...) - c.path = normalizePath(c.path, b2s(c.buf)) -} - -// Domain returns cookie domain. -// -// The returned value is valid until the Cookie reused or released (ReleaseCookie). -// Do not store references to the returned value. Make copies instead. -func (c *Cookie) Domain() []byte { - return c.domain -} - -// SetDomain sets cookie domain. -func (c *Cookie) SetDomain(domain string) { - c.domain = append(c.domain[:0], domain...) -} - -// SetDomainBytes sets cookie domain. -func (c *Cookie) SetDomainBytes(domain []byte) { - c.domain = append(c.domain[:0], domain...) -} - -// MaxAge returns the seconds until the cookie is meant to expire or 0 -// if no max age. -func (c *Cookie) MaxAge() int { - return c.maxAge -} - -// SetMaxAge sets cookie expiration time based on seconds. This takes precedence -// over any absolute expiry set on the cookie. -// -// Set max age to 0 to unset. -func (c *Cookie) SetMaxAge(seconds int) { - c.maxAge = seconds -} - -// Expire returns cookie expiration time. -// -// CookieExpireUnlimited is returned if cookie doesn't expire. -func (c *Cookie) Expire() time.Time { - expire := c.expire - if expire.IsZero() { - expire = cookieExpireUnlimited - } - return expire -} - -// SetExpire sets cookie expiration time. -// -// Set expiration time to CookieExpireDelete for expiring (deleting) -// the cookie on the client. -// -// By default cookie lifetime is limited by browser session. -func (c *Cookie) SetExpire(expire time.Time) { - c.expire = expire -} - -// Value returns cookie value. -// -// The returned value is valid until the Cookie reused or released (ReleaseCookie). -// Do not store references to the returned value. Make copies instead. -func (c *Cookie) Value() []byte { - return c.value -} - -// SetValue sets cookie value. -func (c *Cookie) SetValue(value string) { - c.value = append(c.value[:0], value...) -} - -// SetValueBytes sets cookie value. -func (c *Cookie) SetValueBytes(value []byte) { - c.value = append(c.value[:0], value...) -} - -// Key returns cookie name. -// -// The returned value is valid until the Cookie reused or released (ReleaseCookie). -// Do not store references to the returned value. Make copies instead. -func (c *Cookie) Key() []byte { - return c.key -} - -// SetKey sets cookie name. -func (c *Cookie) SetKey(key string) { - c.key = append(c.key[:0], key...) -} - -// SetKeyBytes sets cookie name. -func (c *Cookie) SetKeyBytes(key []byte) { - c.key = append(c.key[:0], key...) -} - -// Reset clears the cookie. -func (c *Cookie) Reset() { - c.key = c.key[:0] - c.value = c.value[:0] - c.expire = zeroTime - c.maxAge = 0 - c.domain = c.domain[:0] - c.path = c.path[:0] - c.httpOnly = false - c.secure = false - c.sameSite = CookieSameSiteDisabled -} - -// AppendBytes appends cookie representation to dst and returns -// the extended dst. -func (c *Cookie) AppendBytes(dst []byte) []byte { - if len(c.key) > 0 { - dst = append(dst, c.key...) - dst = append(dst, '=') - } - dst = append(dst, c.value...) - - if c.maxAge > 0 { - dst = append(dst, ';', ' ') - dst = append(dst, strCookieMaxAge...) - dst = append(dst, '=') - dst = appendUint(dst, c.maxAge) - } else if !c.expire.IsZero() { - dst = append(dst, ';', ' ') - dst = append(dst, strCookieExpires...) - dst = append(dst, '=') - dst = AppendHTTPDate(dst, c.expire) - } - if len(c.domain) > 0 { - dst = appendCookiePart(dst, strCookieDomain, b2s(c.domain)) - } - if len(c.path) > 0 { - dst = appendCookiePart(dst, strCookiePath, b2s(c.path)) - } - if c.httpOnly { - dst = append(dst, ';', ' ') - dst = append(dst, strCookieHTTPOnly...) - } - if c.secure { - dst = append(dst, ';', ' ') - dst = append(dst, strCookieSecure...) - } - switch c.sameSite { - case CookieSameSiteDefaultMode: - dst = append(dst, ';', ' ') - dst = append(dst, strCookieSameSite...) - case CookieSameSiteLaxMode: - dst = append(dst, ';', ' ') - dst = append(dst, strCookieSameSite...) - dst = append(dst, '=') - dst = append(dst, strCookieSameSiteLax...) - case CookieSameSiteStrictMode: - dst = append(dst, ';', ' ') - dst = append(dst, strCookieSameSite...) - dst = append(dst, '=') - dst = append(dst, strCookieSameSiteStrict...) - case CookieSameSiteNoneMode: - dst = append(dst, ';', ' ') - dst = append(dst, strCookieSameSite...) - dst = append(dst, '=') - dst = append(dst, strCookieSameSiteNone...) - } - return dst -} - -// Cookie returns cookie representation. -// -// The returned value is valid until the Cookie reused or released (ReleaseCookie). -// Do not store references to the returned value. Make copies instead. -func (c *Cookie) Cookie() []byte { - c.buf = c.AppendBytes(c.buf[:0]) - return c.buf -} - -// String returns cookie representation. -func (c *Cookie) String() string { - return string(c.Cookie()) -} - -// WriteTo writes cookie representation to w. -// -// WriteTo implements io.WriterTo interface. -func (c *Cookie) WriteTo(w io.Writer) (int64, error) { - n, err := w.Write(c.Cookie()) - return int64(n), err -} - -var errNoCookies = errors.New("no cookies found") - -// Parse parses Set-Cookie header. -func (c *Cookie) Parse(src string) error { - c.buf = append(c.buf[:0], src...) - return c.ParseBytes(c.buf) -} - -// ParseBytes parses Set-Cookie header. -func (c *Cookie) ParseBytes(src []byte) error { - c.Reset() - ntot := 0 - for { - k, v, n := parseCookie(src) - if n == 0 { - break - } else if ntot == 0 { - c.key = append(c.key, k...) - c.value = append(c.value, v...) - } - key := b2s(k) - value := b2s(v) - ntot += n - src = src[n:] - if len(key) != 0 { - // Case insensitive switch on first char - switch key[0] | 0x20 { - case 'm': - if caseInsensitiveCompare(strCookieMaxAge, key) { - maxAge, err := strconv.ParseUint(value, 10, 32) - if err != nil { - return err - } - c.maxAge = int(maxAge) - } - - case 'e': // "expires" - if caseInsensitiveCompare(strCookieExpires, key) { - - // Try the same two formats as net/http - // See: https://github.com/golang/go/blob/00379be17e63a5b75b3237819392d2dc3b313a27/src/net/http/cookie.go#L133-L135 - exptime, err := time.ParseInLocation(time.RFC1123, value, time.UTC) - if err != nil { - exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", value) - if err != nil { - return err - } - } - c.expire = exptime - } - - case 'd': // "domain" - if caseInsensitiveCompare(strCookieDomain, key) { - c.domain = append(c.domain, value...) - } - - case 'p': // "path" - if caseInsensitiveCompare(strCookiePath, key) { - c.path = append(c.path, value...) - } - - case 's': // "samesite" - if caseInsensitiveCompare(strCookieSameSite, key) { - if len(value) > 0 { - // Case insensitive switch on first char - switch value[0] | 0x20 { - case 'l': // "lax" - if caseInsensitiveCompare(strCookieSameSiteLax, value) { - c.sameSite = CookieSameSiteLaxMode - } - case 's': // "strict" - if caseInsensitiveCompare(strCookieSameSiteStrict, value) { - c.sameSite = CookieSameSiteStrictMode - } - case 'n': // "none" - if caseInsensitiveCompare(strCookieSameSiteNone, value) { - c.sameSite = CookieSameSiteNoneMode - } - } - } - } - } - } else if len(value) != 0 { - // Case insensitive switch on first char - switch value[0] | 0x20 { - case 'h': // "httponly" - if caseInsensitiveCompare(strCookieHTTPOnly, value) { - c.httpOnly = true - } - - case 's': // "secure" - if caseInsensitiveCompare(strCookieSecure, value) { - c.secure = true - } else if caseInsensitiveCompare(strCookieSameSite, value) { - c.sameSite = CookieSameSiteDefaultMode - } - } - } // else empty or no match - } - if len(c.key) == 0 && len(c.value) == 0 { - return errNoCookies - } - return nil -} - -func appendCookiePart(dst []byte, key, value string) []byte { - dst = append(dst, ';', ' ') - dst = append(dst, key...) - dst = append(dst, '=') - return append(dst, value...) -} - -func (hb *headerBuf) appendRequestCookieBytes(dst []byte) []byte { - n := len(hb.cookies) - for i := 0; i < n; i++ { - kv := hb.cookies[i] - if !kv.isValid() { - continue - } else if kv.key.len > 0 { - dst = append(dst, hb.musttoken(kv.key)...) - dst = append(dst, '=') - } - dst = append(dst, hb.musttoken(kv.value)...) - if i+1 < n { - dst = append(dst, ';', ' ') - } - } - return dst -} -func (hb *headerBuf) appendResponseCookieBytes(dst []byte) []byte { - n := len(hb.cookies) - for i := 0; i < n; i++ { - kv := hb.cookies[i] - if !kv.isValid() { - continue - } - dst = append(dst, hb.musttoken(kv.value)...) - if i+1 < n { - dst = append(dst, ';', ' ') - } - } - return dst -} - -type cookieScanner struct { - b []byte -} - -// 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 - } - eqIdx := bytes.IndexByte(cookie, '=') - semiIdx := bytes.IndexByte(cookie, ';') - if eqIdx > 0 && eqIdx < semiIdx { - // cookies has form key=value; - key = trimCookie(cookie[:eqIdx], false) - } else { - // cookie has no key. - eqIdx = -1 // ensure is -1. - } - if semiIdx > 0 { - // found ';' - value = trimCookie(cookie[eqIdx+1:semiIdx], true) - } else { - value = trimCookie(cookie[eqIdx+1:], true) - } - return key, value, max(len(cookie), semiIdx+1) -} - -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 -} - -// caseInsensitiveCompare does a case insensitive equality comparison of -// two []byte. Assumes only letters need to be matched. -func caseInsensitiveCompare(a, b string) bool { - if len(a) != len(b) { - return false - } - for i := 0; i < len(a); i++ { - if a[i]|0x20 != b[i]|0x20 { - return false - } - } - return true -} - -func normalizePath(dst []byte, src string) []byte { - dst = dst[:0] - dst = addLeadingSlash(dst, src) - dst = decodeArgAppendNoPlus(dst, src) - - // remove duplicate slashes - b := dst - bSize := len(b) - for { - n := strings.Index(b2s(b), strSlashSlash) - if n < 0 { - break - } - b = b[n:] - copy(b, b[1:]) - b = b[:len(b)-1] - bSize-- - } - dst = dst[:bSize] - - // remove /./ parts - b = dst - for { - n := strings.Index(b2s(b), strSlashDotSlash) - if n < 0 { - break - } - nn := n + len(strSlashDotSlash) - 1 - copy(b[n:], b[nn:]) - b = b[:len(b)-nn+n] - } - - // remove /foo/../ parts - for { - n := strings.Index(b2s(b), strSlashDotDotSlash) - if n < 0 { - break - } - nn := strings.LastIndexByte(b2s(b[:n]), slashChar) - if nn < 0 { - nn = 0 - } - n += len(strSlashDotDotSlash) - 1 - copy(b[nn:], b[n:]) - b = b[:len(b)-n+nn] - } - - // remove trailing /foo/.. - n := strings.LastIndex(b2s(b), strSlashDotDot) - if n >= 0 && n+len(strSlashDotDot) == len(b) { - nn := strings.LastIndexByte(b2s(b[:n]), slashChar) - if nn < 0 { - return append(dst[:0], slashChar) - } - b = b[:nn+1] - } - - if filepath.Separator == '\\' { - // remove \.\ parts - for { - n := strings.Index(b2s(b), strBackSlashDotBackSlash) - if n < 0 { - break - } - nn := n + len(strSlashDotSlash) - 1 - copy(b[n:], b[nn:]) - b = b[:len(b)-nn+n] - } - - // remove /foo/..\ parts - for { - n := strings.Index(b2s(b), strSlashDotDotBackSlash) - if n < 0 { - break - } - nn := strings.LastIndexByte(b2s(b[:n]), slashChar) - if nn < 0 { - nn = 0 - } - nn++ - n += len(strSlashDotDotBackSlash) - copy(b[nn:], b[n:]) - b = b[:len(b)-n+nn] - } - - // remove /foo\..\ parts - for { - n := strings.Index(b2s(b), strBackSlashDotDotBackSlash) - if n < 0 { - break - } - nn := strings.LastIndexByte(b2s(b[:n]), slashChar) - if nn < 0 { - nn = 0 - } - n += len(strBackSlashDotDotBackSlash) - 1 - copy(b[nn:], b[n:]) - b = b[:len(b)-n+nn] - } - - // remove trailing \foo\.. - n := strings.LastIndex(b2s(b), strBackSlashDotDot) - if n >= 0 && n+len(strSlashDotDot) == len(b) { - nn := strings.LastIndexByte(b2s(b[:n]), slashChar) - if nn < 0 { - return append(dst[:0], slashChar) - } - b = b[:nn+1] - } - } - - return b -} - -func addLeadingSlash(dst []byte, src string) []byte { - // add leading slash for unix paths - if len(src) == 0 || src[0] != slashChar { - dst = append(dst, slashChar) - } - - return dst -} - -// decodeArgAppendNoPlus is almost identical to decodeArgAppend, but it doesn't -// substitute '+' with ' '. -// -// The function is copy-pasted from decodeArgAppend due to the performance -// reasons only. -func decodeArgAppendNoPlus(dst []byte, src string) []byte { - idx := strings.IndexByte(src, '%') - if idx < 0 { - // fast path: src doesn't contain encoded chars - return append(dst, src...) - } - dst = append(dst, src[:idx]...) - - // slow path - for i := idx; i < len(src); i++ { - c := src[i] - if c == '%' { - if i+2 >= len(src) { - return append(dst, src[i:]...) - } - x2 := hex2intTable[src[i+2]] - x1 := hex2intTable[src[i+1]] - if x1 == 16 || x2 == 16 { - dst = append(dst, '%') - } else { - dst = append(dst, x1<<4|x2) - i += 2 - } - } else { - dst = append(dst, c) - } - } - return dst -} - -// AppendHTTPDate appends HTTP-compliant (RFC1123) representation of date -// to dst and returns the extended dst. -func AppendHTTPDate(dst []byte, date time.Time) []byte { - dst = date.In(time.UTC).AppendFormat(dst, time.RFC1123) - copy(dst[len(dst)-3:], strGMT) - return dst -} diff --git a/httpx/definitions.go b/httpx/definitions.go deleted file mode 100644 index ce73fca..0000000 --- a/httpx/definitions.go +++ /dev/null @@ -1,270 +0,0 @@ -package httpx - -const ( - slashChar = '/' - rChar = '\r' - nChar = '\n' - defaultServerName = "fasthttp" - defaultUserAgent = "fasthttp" - defaultContentType = "text/plain; charset=utf-8" -) - -const ( - strSlashSlash = "//" - strSlashDotDot = "/.." - strSlashDotSlash = "/./" - strSlashDotDotSlash = "/../" - strBackSlashDotDot = `\..` - strBackSlashDotBackSlash = `\.\` - strSlashDotDotBackSlash = `/..\` - strBackSlashDotDotBackSlash = `\..\` - strCRLF = "\r\n" - strHTTP = "http" - strHTTPS = "https" - strHTTP10 = "HTTP/1.0" - strHTTP11 = "HTTP/1.1" - strColon = ":" - strColonSlashSlash = "://" - strColonSpace = ": " - strCommaSpace = ", " - strGMT = "GMT" - - strResponseContinue = "HTTP/1.1 100 Continue\r\n\r\n" - - strExpect = HeaderExpect - strConnection = HeaderConnection - strContentLength = HeaderContentLength - strContentType = HeaderContentType - strDate = HeaderDate - strHost = HeaderHost - strReferer = HeaderReferer - strServer = HeaderServer - strTransferEncoding = HeaderTransferEncoding - strContentEncoding = HeaderContentEncoding - strAcceptEncoding = HeaderAcceptEncoding - strUserAgent = HeaderUserAgent - strCookie = HeaderCookie - strSetCookie = HeaderSetCookie - strLocation = HeaderLocation - strIfModifiedSince = HeaderIfModifiedSince - strLastModified = HeaderLastModified - strAcceptRanges = HeaderAcceptRanges - strRange = HeaderRange - strContentRange = HeaderContentRange - strAuthorization = HeaderAuthorization - strTE = HeaderTE - strTrailer = HeaderTrailer - strMaxForwards = HeaderMaxForwards - strProxyConnection = HeaderProxyConnection - strProxyAuthenticate = HeaderProxyAuthenticate - strProxyAuthorization = HeaderProxyAuthorization - strWWWAuthenticate = HeaderWWWAuthenticate - strVary = HeaderVary - - strCookieExpires = "expires" - strCookieDomain = "domain" - strCookiePath = "path" - strCookieHTTPOnly = "HttpOnly" - strCookieSecure = "secure" - strCookieMaxAge = "max-age" - strCookieSameSite = "SameSite" - strCookieSameSiteLax = "Lax" - strCookieSameSiteStrict = "Strict" - strCookieSameSiteNone = "None" - - strClose = "close" - strGzip = "gzip" - strBr = "br" - strDeflate = "deflate" - strKeepAlive = "keep-alive" - strUpgrade = "Upgrade" - strChunked = "chunked" - strIdentity = "identity" - str100Continue = "100-continue" - strPostArgsContentType = "application/x-www-form-urlencoded" - strDefaultContentType = "application/octet-stream" - strMultipartFormData = "multipart/form-data" - strBoundary = "boundary" - strBytes = "bytes" - strBasicSpace = "Basic " - - strApplicationSlash = "application/" - strImageSVG = "image/svg" - strImageIcon = "image/x-icon" - strFontSlash = "font/" - strMultipartSlash = "multipart/" - strTextSlash = "text/" -) - -// Headers. -const ( - // Authentication. - HeaderAuthorization = "Authorization" - HeaderProxyAuthenticate = "Proxy-Authenticate" - HeaderProxyAuthorization = "Proxy-Authorization" - HeaderWWWAuthenticate = "WWW-Authenticate" - - // Caching. - HeaderAge = "Age" - HeaderCacheControl = "Cache-Control" - HeaderClearSiteData = "Clear-Site-Data" - HeaderExpires = "Expires" - HeaderPragma = "Pragma" - HeaderWarning = "Warning" - - // Client hints. - HeaderAcceptCH = "Accept-CH" - HeaderAcceptCHLifetime = "Accept-CH-Lifetime" - HeaderContentDPR = "Content-DPR" - HeaderDPR = "DPR" - HeaderEarlyData = "Early-Data" - HeaderSaveData = "Save-Data" - HeaderViewportWidth = "Viewport-Width" - HeaderWidth = "Width" - - // Conditionals. - HeaderETag = "ETag" - HeaderIfMatch = "If-Match" - HeaderIfModifiedSince = "If-Modified-Since" - HeaderIfNoneMatch = "If-None-Match" - HeaderIfUnmodifiedSince = "If-Unmodified-Since" - HeaderLastModified = "Last-Modified" - HeaderVary = "Vary" - - // Connection management. - HeaderConnection = "Connection" - HeaderKeepAlive = "Keep-Alive" - HeaderProxyConnection = "Proxy-Connection" - - // Content negotiation. - HeaderAccept = "Accept" - HeaderAcceptCharset = "Accept-Charset" - HeaderAcceptEncoding = "Accept-Encoding" - HeaderAcceptLanguage = "Accept-Language" - - // Controls. - HeaderCookie = "Cookie" - HeaderExpect = "Expect" - HeaderMaxForwards = "Max-Forwards" - HeaderSetCookie = "Set-Cookie" - - // CORS. - HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" - HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" - HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" - HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" - HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" - HeaderAccessControlMaxAge = "Access-Control-Max-Age" - HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" - HeaderAccessControlRequestMethod = "Access-Control-Request-Method" - HeaderOrigin = "Origin" - HeaderTimingAllowOrigin = "Timing-Allow-Origin" - HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" - - // Do Not Track. - HeaderDNT = "DNT" - HeaderTk = "Tk" - - // Downloads. - HeaderContentDisposition = "Content-Disposition" - - // Message body information. - HeaderContentEncoding = "Content-Encoding" - HeaderContentLanguage = "Content-Language" - HeaderContentLength = "Content-Length" - HeaderContentLocation = "Content-Location" - HeaderContentType = "Content-Type" - - // Proxies. - HeaderForwarded = "Forwarded" - HeaderVia = "Via" - HeaderXForwardedFor = "X-Forwarded-For" - HeaderXForwardedHost = "X-Forwarded-Host" - HeaderXForwardedProto = "X-Forwarded-Proto" - - // Redirects. - HeaderLocation = "Location" - - // Request context. - HeaderFrom = "From" - HeaderHost = "Host" - HeaderReferer = "Referer" - HeaderReferrerPolicy = "Referrer-Policy" - HeaderUserAgent = "User-Agent" - - // Response context. - HeaderAllow = "Allow" - HeaderServer = "Server" - - // Range requests. - HeaderAcceptRanges = "Accept-Ranges" - HeaderContentRange = "Content-Range" - HeaderIfRange = "If-Range" - HeaderRange = "Range" - - // Security. - HeaderContentSecurityPolicy = "Content-Security-Policy" - HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" - HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy" - HeaderExpectCT = "Expect-CT" - HeaderFeaturePolicy = "Feature-Policy" - HeaderPublicKeyPins = "Public-Key-Pins" - HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only" - HeaderStrictTransportSecurity = "Strict-Transport-Security" - HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests" - HeaderXContentTypeOptions = "X-Content-Type-Options" - HeaderXDownloadOptions = "X-Download-Options" - HeaderXFrameOptions = "X-Frame-Options" - HeaderXPoweredBy = "X-Powered-By" - HeaderXXSSProtection = "X-XSS-Protection" - - // Server-sent event. - HeaderLastEventID = "Last-Event-ID" - HeaderNEL = "NEL" - HeaderPingFrom = "Ping-From" - HeaderPingTo = "Ping-To" - HeaderReportTo = "Report-To" - - // Transfer coding. - HeaderTE = "TE" - HeaderTrailer = "Trailer" - HeaderTransferEncoding = "Transfer-Encoding" - - // WebSockets. - HeaderSecWebSocketAccept = "Sec-WebSocket-Accept" - HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" /* #nosec G101 */ - HeaderSecWebSocketKey = "Sec-WebSocket-Key" - HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol" - HeaderSecWebSocketVersion = "Sec-WebSocket-Version" - - // Other. - HeaderAcceptPatch = "Accept-Patch" - HeaderAcceptPushPolicy = "Accept-Push-Policy" - HeaderAcceptSignature = "Accept-Signature" - HeaderAltSvc = "Alt-Svc" - HeaderDate = "Date" - HeaderIndex = "Index" - HeaderLargeAllocation = "Large-Allocation" - HeaderLink = "Link" - HeaderPushPolicy = "Push-Policy" - HeaderRetryAfter = "Retry-After" - HeaderServerTiming = "Server-Timing" - HeaderSignature = "Signature" - HeaderSignedHeaders = "Signed-Headers" - HeaderSourceMap = "SourceMap" - HeaderUpgrade = "Upgrade" - HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" - HeaderXPingback = "X-Pingback" - HeaderXRequestedWith = "X-Requested-With" - HeaderXRobotsTag = "X-Robots-Tag" - HeaderXUACompatible = "X-UA-Compatible" -) - -// Probably replace these with short functions to take up less program memory -const ( - hex2intTable = "\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x00\x01\x02\x03\x04\x05\x06\a\b\t\x10\x10\x10\x10\x10\x10\x10\n\v\f\r\x0e\x0f\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\n\v\f\r\x0e\x0f\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10" - toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" - toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" - quotedArgShouldEscapeTable = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" - quotedPathShouldEscapeTable = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" -) diff --git a/httpx/header_parse.go b/httpx/header_parse.go deleted file mode 100644 index 16c6875..0000000 --- a/httpx/header_parse.go +++ /dev/null @@ -1,663 +0,0 @@ -package httpx - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io" - "slices" - "strings" - "unsafe" -) - -var ( - errNeedMore = errors.New("need more data: cannot find trailing lf") - errInvalidName = errors.New("invalid header name") - errSmallBuffer = errors.New("small read buffer. Increase ReadBufferSize") - errNonNumericChars = errors.New("non-numeric chars found") -) - -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) readFrom(r io.Reader) error { - buf := hb.buf - free := hb.free() - if free == 0 { - return errSmallBuffer - } - n, err := r.Read(buf[len(buf):cap(buf)]) - hb.buf = buf[:len(buf)+n] - return err -} - -func (h *header) parse() (err error) { - hb := &h.hbuf - hb.off = 0 // start parsing from 0. - h.method, h.requestURI, h.proto, h.flags, err = hb.parseFirstLine(h.flags) - if err != nil { - return err - } - - var ss scannerState - err = h.parseHeaders(&ss) - if err != nil { - return err - } - return nil -} - -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) parseFirstLine(initFlags flags) (method, uri, proto headerSlice, flags flags, err error) { - var b []byte - for len(b) == 0 { - b = hb.scanLine() - } - flags = initFlags - if len(b) < 5 { - return method, uri, proto, flags, errors.New("too short first HTTP line") - } - - methodEnd := max(0, bytes.IndexByte(b, ' ')) - reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ') - if reqURIEnd >= 0 { - reqURIEnd += methodEnd + 1 - } - switch { - case reqURIEnd < 0: - flags |= noHTTP11 - reqURIEnd = methodEnd + 1 - case reqURIEnd == 0: - return method, uri, proto, flags, errors.New("empty URI") - case b2s(b[reqURIEnd+1:]) != strHTTP11: - flags |= noHTTP11 - fallthrough - default: - proto = hb.slice(b[reqURIEnd+1:]) - } - uri = hb.slice(b[methodEnd+1 : reqURIEnd]) - method = hb.slice(b[:methodEnd]) - return method, uri, proto, flags, nil -} - -type scannerState struct { - err error - disableNormalizing bool - - // 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) parseHeaders(ss *scannerState) (err error) { - hb := &h.hbuf - h.contentLength = -2 - - for kv := hb.nextKV2(ss); kv.isValid(); kv = hb.nextKV2(ss) { - if h.flags.hasAny(disableSpecialHeader) { - h.hbuf.headers = append(h.hbuf.headers, kv) - continue - } - } - if ss.err != nil && err == nil { - err = ss.err - } - if err != nil { - h.flags |= connectionClose - return err - } - - // if h.contentLength < 0 { - // h.contentLengthBytes = hb.noKV().value - // } - if h.flags.hasAny(noHTTP11) && !h.flags.hasAny(connectionClose) { - // close connection for non-http/1.1 request unless 'Connection: keep-alive' is set. - if !h.hasHeaderValue(strConnection, strKeepAlive) { - h.flags |= connectionClose - } - } - return nil -} - -func (h *header) hasHeaderValue(key, value string) bool { - kv := h.peekHeader(key) - return kv.isValid() && b2s(h.hbuf.musttoken(kv.value)) == value -} - -func (h *header) peekHeaderBytes(key string) []byte { - kv := h.peekHeader(key) - if kv.isValid() { - return h.hbuf.musttoken(kv.value) - } - return nil -} - -// 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)) - } - 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) - } - slices.Grow(buf, len(key)+len(value)) - } - k := hb.mustAppendSlice(key) - v := hb.mustAppendSlice(value) - if !h.flags.hasAny(disableNormalizing) { - // TODO - } - hb.headers = append(hb.headers, argsKV{ - key: k, - value: v, - }) -} - -func readRawHeaders(dst []byte, buf string) ([]byte, int, error) { - n := strings.IndexByte(buf, nChar) - if n < 0 { - return dst[:0], 0, errNeedMore - } - if (n == 1 && buf[0] == rChar) || n == 0 { - // empty headers - return dst, n + 1, nil - } - - n++ - b := buf - m := n - for { - b = b[m:] - m = strings.IndexByte(b, nChar) - if m < 0 { - return dst, 0, errNeedMore - } - m++ - n += m - if (m == 2 && b[0] == rChar) || m == 1 { - dst = append(dst, buf[:n]...) - return dst, n, nil - } - } -} -func (hb *headerBuf) noKV() argsKV { return argsKV{} } - -func (hb *headerBuf) nextKV2(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]) - normalizeHeaderKey(buf[:n], ss.disableNormalizing) - 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 -} - -func normalizeHeaderKey(b []byte, disableNormalizing bool) { - if disableNormalizing { - return - } - - n := len(b) - if n == 0 { - return - } - - b[0] = toUpperTable[b[0]] - for i := 1; i < n; i++ { - p := &b[i] - if *p == '-' { - i++ - if i < n { - b[i] = toUpperTable[b[i]] - } - continue - } - *p = toLowerTable[*p] - } -} - -func normalizeHeaderValue(ov, ob []byte, headerLength int) (nv, nb []byte, nhl int) { - nv = ov - length := len(ov) - if length <= 0 { - return - } - write := 0 - shrunk := 0 - lineStart := false - for read := 0; read < length; read++ { - c := ov[read] - switch { - case c == rChar || c == nChar: - shrunk++ - if c == nChar { - lineStart = true - } - continue - case lineStart && c == '\t': - c = ' ' - default: - lineStart = false - } - nv[write] = c - write++ - } - - nv = nv[:write] - copy(ob[write:], ob[write+shrunk:]) - - // Check if we need to skip \r\n or just \n - skip := 0 - if ob[write] == rChar { - if ob[write+1] == nChar { - skip += 2 - } else { - skip++ - } - } else if ob[write] == nChar { - skip++ - } - - nb = ob[write+skip : len(ob)-shrunk] - nhl = headerLength - shrunk - return -} - -func parseContentLength(b string) (int, error) { - v, n, err := parseUintBuf(b) - if err != nil { - return -1, fmt.Errorf("cannot parse Content-Length: %w", err) - } - if n != len(b) { - return -1, fmt.Errorf("cannot parse Content-Length: %w", errNonNumericChars) - } - return v, nil -} - -func nextLine(b []byte) ([]byte, []byte, error) { - nNext := bytes.IndexByte(b, nChar) - if nNext < 0 { - return nil, nil, errNeedMore - } - n := nNext - if n > 0 && b[n-1] == rChar { - n-- - } - return b[:n], b[nNext+1:], nil -} - -func stripSpace(b string) string { - for len(b) > 0 && b[0] == ' ' { - b = b[1:] - } - for len(b) > 0 && b[len(b)-1] == ' ' { - b = b[:len(b)-1] - } - return b -} - -var ( - errEmptyInt = errors.New("empty integer") - errUnexpectedFirstChar = errors.New("unexpected first char found. Expecting 0-9") - errUnexpectedTrailingChar = errors.New("unexpected trailing char found. Expecting 0-9") - errTooLongInt = errors.New("too long int") -) - -func parseUintBuf(b string) (int, int, error) { - n := len(b) - if n == 0 { - return -1, 0, errEmptyInt - } - v := 0 - for i := 0; i < n; i++ { - c := b[i] - k := c - '0' - if k > 9 { - if i == 0 { - return -1, i, errUnexpectedFirstChar - } - return v, i, nil - } - vNew := 10*v + int(k) - // Test for overflow. - if vNew < v { - return -1, i, errTooLongInt - } - v = vNew - } - return v, n, nil -} - -/* - -Request Parsing - -*/ - -// Read reads request header from r. -// -// io.EOF is returned if r is closed before reading the first header byte. -func (h *header) Read(r *bufio.Reader) error { - return h.readLoop(r, true) -} - -// readLoop reads request header from r optionally loops until it has enough data. -// -// io.EOF is returned if r is closed before reading the first header byte. -func (h *header) readLoop(r *bufio.Reader, waitForMore bool) error { - n := 1 - for { - err := h.tryRead(r, n) - if err == nil { - return nil - } - if !waitForMore || err != errNeedMore { - h.resetSkipNormalize() - return err - } - n = r.Buffered() + 1 - } -} - -func (h *header) tryRead(r *bufio.Reader, n int) error { - h.resetSkipNormalize() - b, err := r.Peek(n) - - if len(b) == 0 { - if err == io.EOF { - return err - } - - if err == nil { - panic("bufio.Reader.Peek() returned nil, nil") - } - - // This is for go 1.6 bug. See https://github.com/golang/go/issues/14121 . - if err == bufio.ErrBufferFull { - return &ErrSmallBuffer{ - error: fmt.Errorf("error when reading request headers: %w (n=%d, r.Buffered()=%d)", errSmallBuffer, n, r.Buffered()), - } - } - - // n == 1 on the first read for the request. - if n == 1 { - // We didn't read a single byte. - return ErrNothingRead{err} - } - - return fmt.Errorf("error when reading request headers: %w", err) - } - b = mustPeekBuffered(r) - errParse := h.parse() - if errParse != nil { - return headerError("request", err, errParse, b, false) - } - // mustDiscard(r, headersLen) - return nil -} - -func (h *headerBuf) reset() { - *h = headerBuf{ - buf: h.buf[:0], - headers: h.headers[:0], - cookies: h.cookies[:0], - } -} - -func (h *header) resetSkipNormalize() { - h.hbuf.reset() - *h = header{ - hbuf: h.hbuf, - logger: h.logger, - } -} - -func headerError(typ string, err, errParse error, b []byte, secureErrorLogMessage bool) error { - if errParse != errNeedMore { - return headerErrorMsg(typ, errParse, b, secureErrorLogMessage) - } - if err == nil { - return errNeedMore - } - - // Buggy servers may leave trailing CRLFs after http body. - // Treat this case as EOF. - if isOnlyCRLF(b) { - return io.EOF - } - - if err != bufio.ErrBufferFull { - return headerErrorMsg(typ, err, b, secureErrorLogMessage) - } - return &ErrSmallBuffer{ - error: headerErrorMsg(typ, errSmallBuffer, b, secureErrorLogMessage), - } -} - -func isOnlyCRLF(b []byte) bool { - for _, ch := range b { - if ch != rChar && ch != nChar { - return false - } - } - return true -} - -func headerErrorMsg(typ string, err error, b []byte, secureErrorLogMessage bool) error { - return fmt.Errorf("error when reading %s headers: %w. Buffer size=%d", typ, err, len(b)) -} - -// ErrNothingRead is returned when a keep-alive connection is closed, -// either because the remote closed it or because of a read timeout. -type ErrNothingRead struct { - error -} - -// ErrSmallBuffer is returned when the provided buffer size is too small -// for reading request and/or response headers. -// -// ReadBufferSize value from Server or clients should reduce the number -// of such errors. -type ErrSmallBuffer struct { - error -} - -func mustPeekBuffered(r *bufio.Reader) []byte { - buf, err := r.Peek(r.Buffered()) - if len(buf) == 0 || err != nil { - panic(fmt.Sprintf("bufio.Reader.Peek() returned unexpected data (%q, %v)", buf, err)) - } - return buf -} - -func mustDiscard(r *bufio.Reader, n int) { - if _, err := r.Discard(n); err != nil { - panic(fmt.Sprintf("bufio.Reader.Discard(%d) failed: %v", n, err)) - } -} - -// Host returns Host header value. -func (h *header) Host() []byte { - return h.peekHeaderBytes(HeaderHost) -} - -// ConnectionClose returns true if 'Connection: close' header is set. -func (h *header) ConnectionClose() bool { - return h.flags.hasAny(connectionClose) -} - -// UserAgent returns User-Agent header value. -func (h *header) UserAgent() []byte { - return h.peekHeaderBytes(HeaderUserAgent) -} - -// 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)) -} diff --git a/httpx/header_test.go b/httpx/header_test.go deleted file mode 100644 index e16c310..0000000 --- a/httpx/header_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package httpx - -import ( - "bytes" - "net/http" - "strings" - "testing" -) - -func TestHeaderParseRequest(t *testing.T) { - const ( - wantMethod = "GET" - wantURI = "/" - wantMessage = "hello world!" - ) - req, err := http.NewRequest(wantMethod, wantURI, strings.NewReader(wantMessage)) - if err != nil { - t.Fatal(err) - } - var buf bytes.Buffer - req.Write(&buf) - var hdr header - err = hdr.ParseBytes(buf.Bytes()) - if err != nil { - t.Fatal(err) - } - if !hdr.MethodIs(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()) - } -} diff --git a/httpx/tokenizer.go b/httpx/tokenizer.go deleted file mode 100644 index 0164a10..0000000 --- a/httpx/tokenizer.go +++ /dev/null @@ -1,337 +0,0 @@ -package httpx - -import ( - "errors" - "log/slog" - "net/http" - "strconv" - "unsafe" - - "github.com/soypat/lneto/internal" -) - -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 - cookies []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. -} - -func (kv argsKV) isValid() bool { - return kv.key.start > 0 -} - -func (kv *argsKV) invalidate() { - *kv = argsKV{} -} - -func (tb headerBuf) musttoken(slice headerSlice) []byte { - return tb.buf[slice.start : slice.start+slice.len] -} - -func (tb headerBuf) slice(b []byte) headerSlice { - base := uintptr(unsafe.Pointer(unsafe.SliceData(tb.buf))) - off := uintptr(unsafe.Pointer(unsafe.SliceData(b))) - if off < base || off > base+uintptr(len(tb.buf)) { - panic("httpx: argument buffer does not alias header buffer") - } - return headerSlice{ - start: tokint(off - base), - len: tokint(len(b)), - } -} - -func (kv argsKV) HasValue() bool { return kv.value.start > 0 } - -type flags uint8 - -const ( - disableNormalizing flags = 1 << iota - disableSpecialHeader - noDefaultContentType - connectionClose - noHTTP11 - cookiesCollected - flagNoBufferGrow - flagOOMReached -) - -func (f flags) hasAny(checkThese flags) bool { - return f&checkThese != 0 -} - -type header struct { - hbuf headerBuf - logger *slog.Logger - contentLength int - - method headerSlice - requestURI headerSlice - proto headerSlice - - flags flags -} - -func (h *header) ParseBytes(b []byte) error { - h.resetSkipNormalize() - h.hbuf.readFromBytes(b) - return h.parse() -} - -func (h *header) Set(key, value string) { - h.SetCanonical(key, value) //TODO: implement non-canonical. -} - -func (h *header) Add(key, value string) { - h.appendHeader(key, value) -} - -// ContentType returns Content-Type header value. -func (h *header) ContentType() []byte { - return h.peekHeaderBytes(HeaderContentType) -} - -// SetCanonical sets the given 'key: value' header assuming that -// key is in canonical form. -// -// If the header is set as a Trailer (forbidden trailers will not be set, see SetTrailer for more details), -// it will be sent after the chunked request body. -func (h *header) SetCanonical(key, value string) { - kv := h.peekPtrHeader(key) - if kv != nil { - kv.invalidate() - } - h.appendHeader(key, value) -} - -// SetHost sets Host header value. -func (h *header) SetHost(host string) { - h.Set(HeaderHost, host) -} - -// SetUserAgent sets User-Agent header value. -func (h *header) SetUserAgent(userAgent string) { - h.Set(HeaderUserAgent, userAgent) -} - -// SetConnectionClose sets 'Connection: close' header. -func (h *header) SetConnectionClose() { - h.flags |= connectionClose -} - -// ResetConnectionClose clears 'Connection: close' header if it exists. -func (h *header) ResetConnectionClose() { - if h.flags.hasAny(connectionClose) { - h.flags &^= connectionClose - // h.h = delAllArgs(h.h, strConnection) // TODO - } -} - -func appendUint(b []byte, v int) []byte { - if v < 0 { - panic("negative uint") - } - return strconv.AppendUint(b, uint64(v), 10) -} - -// ContentLength returns Content-Length header value. -// -// It may be negative: -// -1 means Transfer-Encoding: chunked. -// -2 means Transfer-Encoding: identity. -func (h *header) ContentLength() int { - return h.contentLength -} - -var ErrBadTrailer = errors.New("contain forbidden trailer") - -// DisableNormalizing disables header names' normalization. -// -// By default all the header names are normalized by uppercasing -// the first letter and all the first letters following dashes, -// while lowercasing all the other letters. -// Examples: -// -// - CONNECTION -> Connection -// - conteNT-tYPE -> Content-Type -// - foo-bar-baz -> Foo-Bar-Baz -// -// Disable header names' normalization only if know what are you doing. -func (h *header) DisableNormalizing() { - h.flags |= disableNormalizing -} - -// Method returns HTTP request method. -func (h *header) Method() []byte { - return h.hbuf.musttoken(h.method) -} - -func (h *header) SetMethod(method string) { - h.method = h.reuseOrAppend(h.method, method) -} - -// SetRequestURI sets RequestURI for the first HTTP request line. -func (h *header) SetRequestURI(requestURI string) { - h.requestURI = h.reuseOrAppend(h.requestURI, requestURI) -} - -// RequestURI returns RequestURI from the first HTTP request line. -func (h *header) RequestURI() []byte { - if h.requestURI.start == 0 { - return nil - } else if h.requestURI.len == 0 { - h.requestURI = h.appendSlice("/") - } - return h.hbuf.musttoken(h.requestURI) -} - -// Protocol returns HTTP protocol. -func (h *header) Protocol() []byte { - if h.proto.len == 0 { - h.proto = h.appendSlice(strHTTP11) - } - return h.hbuf.musttoken(h.proto) -} - -func (h *header) SetProtocol(protocol string) { - h.proto = h.reuseOrAppend(h.proto, protocol) -} - -// AppendReqRespCommon appends request/response common header representation to dst and returns the extended buffer. -func (h *header) AppendReqRespCommon(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)) - } - } - - // if len(h.trailer) > 0 { - // aux := appendArgsKey(nil, h.trailer, strCommaSpace) - // dst = appendHeaderLine(dst, strTrailer, b2s(aux)) - // } - - // there is no need in h.collectCookies() here, since if cookies aren't collected yet, - // they all are located in h.h. - n := len(h.hbuf.cookies) - if n > 0 && !h.flags.hasAny(disableSpecialHeader) { - dst = append(dst, strCookie...) - dst = append(dst, strColonSpace...) - h.hbuf.appendRequestCookieBytes(dst) - dst = append(dst, strCRLF...) - } - - if h.ConnectionClose() && !h.flags.hasAny(disableSpecialHeader) { - dst = appendHeaderLine(dst, strConnection, strClose) - } - - return append(dst, strCRLF...) -} - -func appendHeaderLine(dst []byte, key, value string) []byte { - dst = append(dst, key...) - dst = append(dst, strColonSpace...) - dst = append(dst, value...) - return append(dst, strCRLF...) -} - -func (h *header) ignoreBody() bool { - return h.IsGet() || h.IsHead() -} - -func (h *header) collectCookies() { - if h.flags.hasAny(cookiesCollected) { - return - } - n := len(h.hbuf.headers) - for i := 0; i < n; i++ { - kv := h.hbuf.headers[i] - if kv.isValid() && caseInsensitiveCompare(b2s(h.hbuf.musttoken(kv.key)), HeaderCookie) { - cookie := h.hbuf.musttoken(kv.value) - for len(cookie) > 0 { - key, value, n := parseCookie(cookie) - h.hbuf.cookies = append(h.hbuf.cookies, argsKV{ - key: h.hbuf.slice(key), - value: h.hbuf.slice(value), - }) - cookie = cookie[n:] - } - } - } - h.flags |= cookiesCollected -} - -func (h *header) parseReqCookie(value []byte) { - -} - -func (h *header) MethodIs(method string) bool { - return b2s(h.Method()) == method -} - -// IsGet returns true if request method is GET. -func (h *header) IsGet() bool { return h.method.len == 0 || h.MethodIs(http.MethodGet) } - -// IsHead returns true if request method is HEAD. -func (h *header) IsHead() bool { return h.MethodIs(http.MethodHead) } - -// IsPost returns true if request method is POST. -func (h *header) IsPost() bool { return h.MethodIs(http.MethodPost) } - -// IsPut returns true if request method is PUT. -func (h *header) IsPut() bool { return h.MethodIs(http.MethodPut) } - -// IsDelete returns true if request method is DELETE. -func (h *header) IsDelete() bool { return h.MethodIs(http.MethodDelete) } - -// IsConnect returns true if request method is CONNECT. -func (h *header) IsConnect() bool { return h.MethodIs(http.MethodConnect) } - -// IsOptions returns true if request method is OPTIONS. -func (h *header) IsOptions() bool { return h.MethodIs(http.MethodOptions) } - -// IsTrace returns true if request method is TRACE. -func (h *header) IsTrace() bool { return h.MethodIs(http.MethodTrace) } - -// IsPatch returns true if request method is PATCH. -func (h *header) IsPatch() bool { return h.MethodIs(http.MethodPatch) } - -// IsHTTP11 returns true if the request is HTTP/1.1. -func (h *header) IsHTTP11() bool { return !h.flags.hasAny(noHTTP11) } - -// 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() {} - -func (h *header) trace(msg string, attrs ...slog.Attr) { - internal.LogAttrs(h.logger, internal.LevelTrace, msg, attrs...) -} -func (h *header) debug(msg string, attrs ...slog.Attr) { - internal.LogAttrs(h.logger, slog.LevelDebug, msg, attrs...) -} -func (h *header) info(msg string, attrs ...slog.Attr) { - internal.LogAttrs(h.logger, slog.LevelInfo, msg, attrs...) -}