mirror of
https://github.com/soypat/lneto.git
synced 2026-09-04 05:49:02 +00:00
apply @MDr164 various fixes
This commit is contained in:
+12
-11
@@ -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
|
// the bytes that follow the parsed request header. Read the request body with
|
||||||
// [Exchange.ReadBody] before setting response headers.
|
// [Exchange.ReadBody] before setting response headers.
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
used atomic.Bool
|
acquired atomic.Bool
|
||||||
gen atomic.Uint32
|
gen atomic.Uint32
|
||||||
respTopBuf [maxStatusLine]byte
|
respTopBuf [maxStatusLine]byte
|
||||||
respTopWritten uint8
|
respTopWritten uint8
|
||||||
@@ -105,7 +105,7 @@ func (exch *Exchange) Configure(cfg ExchangeConfig) {
|
|||||||
// reusing the buffer set by [Exchange.Configure]. Returns false if the exchange
|
// reusing the buffer set by [Exchange.Configure]. Returns false if the exchange
|
||||||
// is already serving, in which case conn is untouched.
|
// is already serving, in which case conn is untouched.
|
||||||
func (exch *Exchange) Acquire(conn conn) bool {
|
func (exch *Exchange) Acquire(conn conn) bool {
|
||||||
if !exch.used.CompareAndSwap(false, true) {
|
if !exch.acquired.CompareAndSwap(false, true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
exch.matchedPattern = ""
|
exch.matchedPattern = ""
|
||||||
@@ -133,7 +133,7 @@ func (exch *Exchange) Release() {
|
|||||||
}
|
}
|
||||||
exch.rw = nil
|
exch.rw = nil
|
||||||
exch.gen.Add(1)
|
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.
|
// 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
|
// 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 {
|
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 {
|
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
|
// 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.
|
// has no body at all, RFC 9112 6.3. Check for the error to answer 411 instead.
|
||||||
// See [httpraw.Header.ContentLength].
|
// See [httpraw.Header.ContentLength].
|
||||||
func (exch *Exchange) RequestContentLength() (int64, error) {
|
func (exch *Exchange) RequestContentLength() (int64, bool, error) {
|
||||||
return exch.RequestHeaderRaw().ContentLength()
|
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.
|
// wire would parse chunk sizes as form data. httpraw does not decode them.
|
||||||
return errUnsupportedTransferCoding
|
return errUnsupportedTransferCoding
|
||||||
}
|
}
|
||||||
length, err := exch.RequestContentLength()
|
length, present, err := exch.RequestContentLength()
|
||||||
if err != nil {
|
if !present {
|
||||||
dst.Reset(buf[:0])
|
return nil // No length is no body, RFC 9112 6.3.
|
||||||
return dst.Parse() // No length is no body, RFC 9112 6.3.
|
} else if err != nil {
|
||||||
|
return err
|
||||||
} else if length > int64(len(buf)) {
|
} 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]
|
buf = buf[:length]
|
||||||
for read := 0; read < len(buf); {
|
for read := 0; read < len(buf); {
|
||||||
|
|||||||
@@ -707,7 +707,7 @@ func TestExchangeRequestParseForm(t *testing.T) {
|
|||||||
name: "body larger than buffer",
|
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",
|
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,
|
bufSize: 4,
|
||||||
wantErr: lneto.ErrBufferFull,
|
wantErr: lneto.ErrShortBuffer,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
+6
-2
@@ -13,6 +13,9 @@ import (
|
|||||||
// Requires exchange to be acquired and configured. Will panic if any argument is nil.
|
// 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.
|
// Handle does not close the connection on any outcome: the caller owns it.
|
||||||
func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
||||||
|
if !exch.acquired.Load() {
|
||||||
|
return lneto.ErrBadState
|
||||||
|
}
|
||||||
reqhdr := &exch.reqHdr
|
reqhdr := &exch.reqHdr
|
||||||
reqhdr.Reset(nil, 0) // Assume exchange has been configured and reuse memory.
|
reqhdr.Reset(nil, 0) // Assume exchange has been configured and reuse memory.
|
||||||
var consecutiveBackoffs uint
|
var consecutiveBackoffs uint
|
||||||
@@ -56,11 +59,12 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
|||||||
if handler != nil {
|
if handler != nil {
|
||||||
exch.matchedPattern = matchedPattern
|
exch.matchedPattern = matchedPattern
|
||||||
handler(exch)
|
handler(exch)
|
||||||
exch.FlushHeader()
|
if !exch.hijacked {
|
||||||
|
exch.FlushHeader()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
exch.WriteHeader(404)
|
exch.WriteHeader(404)
|
||||||
}
|
}
|
||||||
// TODO write response from exchange here.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-7
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -100,18 +101,44 @@ type RouterConfig struct {
|
|||||||
Logger *slog.Logger
|
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
|
// Validate returns a non-nil error if the configuration cannot be used to
|
||||||
// configure a [Router].
|
// configure a [Router].
|
||||||
func (cfg RouterConfig) Validate() error {
|
func (cfg RouterConfig) Validate() error {
|
||||||
workerMode := cfg.workerMode()
|
workerMode := cfg.workerMode()
|
||||||
if workerMode && cfg.MaxAwaitingConns == 0 ||
|
switch {
|
||||||
cfg.Mux == nil ||
|
case cfg.Mux == nil,
|
||||||
cfg.RequestNumHeaderKVCap <= 0 ||
|
!workerMode && cfg.FixedNumGoroutines != -1,
|
||||||
!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
|
return lneto.ErrInvalidConfig
|
||||||
} else if cfg.Backoff == nil {
|
case cfg.Backoff == nil:
|
||||||
return lneto.ErrMissingHALConfig
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +242,7 @@ func (r *Router) awaitIdleExchangesLocked(maxWait time.Duration) error {
|
|||||||
for waited := time.Duration(0); ; waited += pollInterval {
|
for waited := time.Duration(0); ; waited += pollInterval {
|
||||||
busy := false
|
busy := false
|
||||||
for i := range r.exchs {
|
for i := range r.exchs {
|
||||||
if r.exchs[i].used.Load() {
|
if r.exchs[i].acquired.Load() {
|
||||||
busy = true
|
busy = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -269,7 +296,7 @@ func (r *Router) Handle(conn io.ReadWriteCloser) error {
|
|||||||
enqueued = true
|
enqueued = true
|
||||||
default:
|
default:
|
||||||
// pendingConns cannot store another Conn, we drop and return error.
|
// pendingConns cannot store another Conn, we drop and return error.
|
||||||
exch.used.Store(false) // release.
|
exch.acquired.Store(false) // release.
|
||||||
}
|
}
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
if enqueued {
|
if enqueued {
|
||||||
|
|||||||
+57
-13
@@ -335,7 +335,7 @@ func (h *Header) takeReusableSlot(key string) *argsKV {
|
|||||||
return useKv
|
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 {
|
func (h *Header) Get(key string) []byte {
|
||||||
debuglog("http:get:start")
|
debuglog("http:get:start")
|
||||||
kv := h.peekHeader(key)
|
kv := h.peekHeader(key)
|
||||||
@@ -347,26 +347,70 @@ func (h *Header) Get(key string) []byte {
|
|||||||
return nil
|
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.
|
// 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
|
// 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.
|
// 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
|
// The value must be digits only, so a negative or list-valued field is rejected
|
||||||
// rather than guessed at.
|
// rather than guessed at.
|
||||||
func (h *Header) ContentLength() (int64, error) {
|
func (h *Header) ContentLength() (int64, bool, error) {
|
||||||
kv := h.peekHeader(headerContentLength)
|
value := h.GetFold(headerContentLength)
|
||||||
if !kv.isValid() {
|
if value == nil {
|
||||||
return 0, errNoContentLength
|
return 0, false, nil
|
||||||
}
|
|
||||||
value := trimOWS(h.hbuf.musttoken(kv.value))
|
|
||||||
if len(value) == 0 {
|
|
||||||
return 0, errBadContentLength
|
|
||||||
}
|
}
|
||||||
|
value = trimOWS(value)
|
||||||
// Unsigned parse of 63 bits rejects a sign and anything past int64's range.
|
// Unsigned parse of 63 bits rejects a sign and anything past int64's range.
|
||||||
n, err := strconv.ParseUint(b2s(value), 10, 63)
|
n, err := strconv.ParseInt(b2s(value), 10, 64)
|
||||||
if err != nil {
|
if err != nil || n < 0 {
|
||||||
return 0, errBadContentLength // strconv's error allocates and is not comparable.
|
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.
|
// Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer.
|
||||||
|
|||||||
@@ -200,12 +200,15 @@ func TestHeaderContentLength(t *testing.T) {
|
|||||||
if err := h.ParseBytes(false, []byte(raw+"\r\n")); err != nil {
|
if err := h.ParseBytes(false, []byte(raw+"\r\n")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
got, err := h.ContentLength()
|
got, present, err := h.ContentLength()
|
||||||
if err != test.wantErr {
|
if err != test.wantErr {
|
||||||
t.Errorf("%q: want error %v, got %v", test.field, test.wantErr, err)
|
t.Errorf("%q: want error %v, got %v", test.field, test.wantErr, err)
|
||||||
} else if err == nil && got != test.want {
|
} else if err == nil && got != test.want {
|
||||||
t.Errorf("%q: want %d, got %d", test.field, test.want, got)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user