diff --git a/http/httphi/exchange.go b/http/httphi/exchange.go index f7d2126..4ed1fd9 100644 --- a/http/httphi/exchange.go +++ b/http/httphi/exchange.go @@ -25,7 +25,7 @@ const maxStatusLine = len("HTTP/1.1 ") + 3 + 1 + len("Network Authentication Req // the bytes that follow the parsed request header. Read the request body with // [Exchange.ReadBody] before setting response headers. type Exchange struct { - used atomic.Bool + acquired atomic.Bool gen atomic.Uint32 respTopBuf [maxStatusLine]byte respTopWritten uint8 @@ -105,7 +105,7 @@ func (exch *Exchange) Configure(cfg ExchangeConfig) { // reusing the buffer set by [Exchange.Configure]. Returns false if the exchange // is already serving, in which case conn is untouched. func (exch *Exchange) Acquire(conn conn) bool { - if !exch.used.CompareAndSwap(false, true) { + if !exch.acquired.CompareAndSwap(false, true) { return false } exch.matchedPattern = "" @@ -133,7 +133,7 @@ func (exch *Exchange) Release() { } exch.rw = nil exch.gen.Add(1) - exch.used.Store(false) + exch.acquired.Store(false) } // UnsafeRawBuffer returns the contiguous buffer owned by [Exchange] being used for the request and response. @@ -278,9 +278,9 @@ type ExchangeRW struct { } // IsValid returns true while the handle still refers to the request it was -// taken from, i.e: false once the exchange was released or hijacked away. +// taken from, i.e: false once the exchange was released. func (rw *ExchangeRW) IsValid() bool { - return rw.gen == rw.exch.gen.Load() && rw.exch.used.Load() + return rw.gen == rw.exch.gen.Load() && rw.exch.acquired.Load() } func (rw *ExchangeRW) validate() error { @@ -407,7 +407,7 @@ func (exch *Exchange) RequestContentType() []byte { // Content-Length field. An absent field is not a client error: such a request // has no body at all, RFC 9112 6.3. Check for the error to answer 411 instead. // See [httpraw.Header.ContentLength]. -func (exch *Exchange) RequestContentLength() (int64, error) { +func (exch *Exchange) RequestContentLength() (int64, bool, error) { return exch.RequestHeaderRaw().ContentLength() } @@ -432,12 +432,13 @@ func (exch *Exchange) RequestParseForm(dst *httpraw.Form, buf []byte) error { // wire would parse chunk sizes as form data. httpraw does not decode them. return errUnsupportedTransferCoding } - length, err := exch.RequestContentLength() - if err != nil { - dst.Reset(buf[:0]) - return dst.Parse() // No length is no body, RFC 9112 6.3. + length, present, err := exch.RequestContentLength() + if !present { + return nil // No length is no body, RFC 9112 6.3. + } else if err != nil { + return err } else if length > int64(len(buf)) { - return lneto.ErrBufferFull // Refuse before reading, caller may answer 413. + return lneto.ErrShortBuffer // Refuse before reading, caller may answer 413. } buf = buf[:length] for read := 0; read < len(buf); { diff --git a/http/httphi/exchange_test.go b/http/httphi/exchange_test.go index 1614515..5c04d34 100644 --- a/http/httphi/exchange_test.go +++ b/http/httphi/exchange_test.go @@ -707,7 +707,7 @@ func TestExchangeRequestParseForm(t *testing.T) { name: "body larger than buffer", request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 11\r\n\r\na=1&b=2&c=3", bufSize: 4, - wantErr: lneto.ErrBufferFull, + wantErr: lneto.ErrShortBuffer, }, } { t.Run(test.name, func(t *testing.T) { diff --git a/http/httphi/mux.go b/http/httphi/mux.go index 6565eee..0e6e4e4 100644 --- a/http/httphi/mux.go +++ b/http/httphi/mux.go @@ -13,6 +13,9 @@ import ( // Requires exchange to be acquired and configured. Will panic if any argument is nil. // Handle does not close the connection on any outcome: the caller owns it. func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error { + if !exch.acquired.Load() { + return lneto.ErrBadState + } reqhdr := &exch.reqHdr reqhdr.Reset(nil, 0) // Assume exchange has been configured and reuse memory. var consecutiveBackoffs uint @@ -56,11 +59,12 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error { if handler != nil { exch.matchedPattern = matchedPattern handler(exch) - exch.FlushHeader() + if !exch.hijacked { + exch.FlushHeader() + } } else { exch.WriteHeader(404) } - // TODO write response from exchange here. return nil } diff --git a/http/httphi/router.go b/http/httphi/router.go index c9ab08b..1c21ef4 100644 --- a/http/httphi/router.go +++ b/http/httphi/router.go @@ -4,6 +4,7 @@ import ( "errors" "io" "log/slog" + "math" "sync" "sync/atomic" "time" @@ -100,18 +101,44 @@ type RouterConfig struct { Logger *slog.Logger } +const ( + // minRequestHeaderBuffer is the smallest request buffer [httpraw.Header] + // accepts with buffer growth disabled, which is how exchanges are configured. + minRequestHeaderBuffer = 32 + // minResponseHeaderBuffer is the room [Exchange.FlushHeader] needs for the + // CRLF closing the header block, written even when no field was staged. + minResponseHeaderBuffer = len("\r\n") + // maxExchangeBuffer bounds an exchange's whole buffer: [Exchange] indexes it + // with uint16 offsets, so a larger one would be addressed truncated. + maxExchangeBuffer = math.MaxUint16 +) + // Validate returns a non-nil error if the configuration cannot be used to // configure a [Router]. func (cfg RouterConfig) Validate() error { workerMode := cfg.workerMode() - if workerMode && cfg.MaxAwaitingConns == 0 || - cfg.Mux == nil || - cfg.RequestNumHeaderKVCap <= 0 || - !workerMode && cfg.FixedNumGoroutines != -1 { + switch { + case cfg.Mux == nil, + !workerMode && cfg.FixedNumGoroutines != -1, + workerMode && cfg.MaxAwaitingConns <= 0, + cfg.RequestNumHeaderKVCap <= 0, + cfg.RequestHeaderBufferSize < minRequestHeaderBuffer, + cfg.ResponseHeaderMinBufferSize < minResponseHeaderBuffer, + cfg.ResponseHeaderMinBufferSize > maxExchangeBuffer, + cfg.RequestHeaderBufferSize > maxExchangeBuffer-cfg.ResponseHeaderMinBufferSize: return lneto.ErrInvalidConfig - } else if cfg.Backoff == nil { + case cfg.Backoff == nil: return lneto.ErrMissingHALConfig } + if workerMode { + // Buffer sizes are bounded by maxExchangeBuffer above so the sum cannot + // overflow; the products below allocate and can. + exchBuf := cfg.RequestHeaderBufferSize + cfg.ResponseHeaderMinBufferSize + if cfg.FixedNumGoroutines > math.MaxInt/exchBuf || + cfg.FixedNumGoroutines > math.MaxInt/cfg.RequestNumHeaderKVCap { + return lneto.ErrInvalidConfig + } + } return nil } @@ -215,7 +242,7 @@ func (r *Router) awaitIdleExchangesLocked(maxWait time.Duration) error { for waited := time.Duration(0); ; waited += pollInterval { busy := false for i := range r.exchs { - if r.exchs[i].used.Load() { + if r.exchs[i].acquired.Load() { busy = true break } @@ -269,7 +296,7 @@ func (r *Router) Handle(conn io.ReadWriteCloser) error { enqueued = true default: // pendingConns cannot store another Conn, we drop and return error. - exch.used.Store(false) // release. + exch.acquired.Store(false) // release. } r.mu.Unlock() if enqueued { diff --git a/http/httpraw/header.go b/http/httpraw/header.go index b96d695..9150481 100644 --- a/http/httpraw/header.go +++ b/http/httpraw/header.go @@ -335,7 +335,7 @@ func (h *Header) takeReusableSlot(key string) *argsKV { return useKv } -// Get gets the first value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key. +// Get gets the first exact-match value of a key found in the headers. Use [Header.ForEach] to find multiple values corresponding to same key. func (h *Header) Get(key string) []byte { debuglog("http:get:start") kv := h.peekHeader(key) @@ -347,26 +347,70 @@ func (h *Header) Get(key string) []byte { return nil } +// GetFold gets the first value whose key matches key under ASCII case-insensitive +// comparison, i.e: "content-length" matches "Content-Length". +// Use [Header.Get] for exact match and [Header.ForEach] to find multiple values +// corresponding to same key. +func (h *Header) GetFold(key string) []byte { + hb := &h.hbuf + for i := 0; i < len(hb.headers); i++ { + kv := hb.headers[i] + if kv.isValid() && asciiEqualFold(b2s(hb.musttoken(kv.key)), key) { + return hb.musttoken(kv.value) + } + } + return nil +} + +// asciiEqualFold reports whether a and b are equal under ASCII case folding. +// Unlike strings.EqualFold it does not fold non-ASCII runes, so no multi-byte +// rune such as U+212A KELVIN SIGN can alias a header key. +func asciiEqualFold(a, b string) bool { + if len(a) != len(b) { + return false + } + const asciiCapDiff = 'a' - 'A' + for i := 0; i < len(a); i++ { + ca, cb := a[i], b[i] + if ca >= 'A' && ca <= 'Z' { + ca += asciiCapDiff + } + if cb >= 'A' && cb <= 'Z' { + cb += asciiCapDiff + } + if ca != cb { + return false + } + } + return true +} + +// NormalizeKeys normalizes all header keys. i.e: CONTENT-type -> Content-Type +func (h *Header) NormalizeKeys() { + for _, kv := range h.hbuf.headers { + if kv.isValid() { + NormalizeHeaderKey(h.hbuf.musttoken(kv.key)) + } + } +} + // ContentLength returns the body length declared by the Content-Length field. // Fails with an error if the field is absent, which for a request means the // message has no body at all unless a transfer coding applies, RFC 9112 6.3. // The value must be digits only, so a negative or list-valued field is rejected // rather than guessed at. -func (h *Header) ContentLength() (int64, error) { - kv := h.peekHeader(headerContentLength) - if !kv.isValid() { - return 0, errNoContentLength - } - value := trimOWS(h.hbuf.musttoken(kv.value)) - if len(value) == 0 { - return 0, errBadContentLength +func (h *Header) ContentLength() (int64, bool, error) { + value := h.GetFold(headerContentLength) + if value == nil { + return 0, false, nil } + value = trimOWS(value) // Unsigned parse of 63 bits rejects a sign and anything past int64's range. - n, err := strconv.ParseUint(b2s(value), 10, 63) - if err != nil { - return 0, errBadContentLength // strconv's error allocates and is not comparable. + n, err := strconv.ParseInt(b2s(value), 10, 64) + if err != nil || n < 0 { + return n, true, errBadContentLength // strconv's error allocates and is not comparable. } - return int64(n), nil + return n, true, nil } // Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer. diff --git a/http/httpraw/header_test.go b/http/httpraw/header_test.go index 1ac6288..40a3a77 100644 --- a/http/httpraw/header_test.go +++ b/http/httpraw/header_test.go @@ -200,12 +200,15 @@ func TestHeaderContentLength(t *testing.T) { if err := h.ParseBytes(false, []byte(raw+"\r\n")); err != nil { t.Fatal(err) } - got, err := h.ContentLength() + got, present, err := h.ContentLength() if err != test.wantErr { t.Errorf("%q: want error %v, got %v", test.field, test.wantErr, err) } else if err == nil && got != test.want { t.Errorf("%q: want %d, got %d", test.field, test.want, got) } + if strings.EqualFold(test.field, headerContentLength) != present { + t.Error("unexpected 'present'", test.field) + } } }