mirror of
https://github.com/soypat/lneto.git
synced 2026-08-11 02:13:44 +00:00
332 lines
8.6 KiB
Go
332 lines
8.6 KiB
Go
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(&tb.buf[0]))
|
|
off := uintptr(unsafe.Pointer(&b[0]))
|
|
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) 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...)
|
|
}
|