diff --git a/examples/berkeley-listener/berkeley_server.go b/examples/berkeley-listener/berkeley_server.go index 310dffb..5a6ac7c 100644 --- a/examples/berkeley-listener/berkeley_server.go +++ b/examples/berkeley-listener/berkeley_server.go @@ -274,7 +274,7 @@ func handleConnNet(conn net.Conn) error { defer conn.Close() conn.SetDeadline(time.Now().Add(10 * time.Second)) - var hdr httpraw.Header + var hdr httpraw.HeaderV1 needMore := true for needMore { _, err := hdr.ReadFromLimited(conn, 1024) @@ -291,7 +291,7 @@ func handleConnNet(conn net.Conn) error { uri := string(hdr.RequestTarget()) fmt.Printf("< %s %s\n", method, uri) - var resp httpraw.Header + var resp httpraw.HeaderV1 resp.SetProtocol("HTTP/1.1") resp.SetStatus("200", "OK") resp.Set("Content-Type", "text/html") @@ -368,7 +368,7 @@ func mockClient(stack *xnet.StackAsync, port uint16, subnet netip.Prefix) { panic("mock client deadline exceeded to establish") } - var hdr httpraw.Header + var hdr httpraw.HeaderV1 hdr.SetMethod("GET") hdr.SetRequestTarget("/") hdr.SetProtocol("HTTP/1.1") diff --git a/examples/httpclient/main.go b/examples/httpclient/main.go index 8fbd6be..5aa8eb9 100644 --- a/examples/httpclient/main.go +++ b/examples/httpclient/main.go @@ -25,7 +25,7 @@ func run() error { flag.IntVar(&port, "lport", 13337, "Local port over which to hit server") flag.Parse() // Prepare GET request. - var hdr httpraw.Header + var hdr httpraw.HeaderV1 hdr.SetMethod("GET") hdr.SetRequestTarget("/") hdr.SetProtocol("HTTP/1.1") diff --git a/examples/httpserver/main.go b/examples/httpserver/main.go index 7f55fff..953a4fe 100644 --- a/examples/httpserver/main.go +++ b/examples/httpserver/main.go @@ -267,7 +267,7 @@ func handleConnection(conn *tcp.Conn) error { conn.SetDeadline(time.Now().Add(10 * time.Second)) // Read HTTP request. - var hdr httpraw.Header + var hdr httpraw.HeaderV1 var needMore bool = true for needMore { _, err := hdr.ReadFromLimited(conn, 1024) @@ -288,7 +288,7 @@ func handleConnection(conn *tcp.Conn) error { // Build response body. // Build HTTP response. - var resp httpraw.Header + var resp httpraw.HeaderV1 resp.SetProtocol("HTTP/1.1") resp.SetStatus("200", "OK") resp.Set("Content-Type", "text/html") diff --git a/examples/xcurl/main.go b/examples/xcurl/main.go index 000d991..9817926 100644 --- a/examples/xcurl/main.go +++ b/examples/xcurl/main.go @@ -305,7 +305,7 @@ func run() (err error) { }) timeHTTPCreate := timer("create HTTP GET request") - var hdr httpraw.Header + var hdr httpraw.HeaderV1 hdr.SetMethod("GET") hdr.SetRequestTarget("/") hdr.SetProtocol("HTTP/1.1") diff --git a/http/httphi/exchange.go b/http/httphi/exchange.go index a414769..289c28b 100644 --- a/http/httphi/exchange.go +++ b/http/httphi/exchange.go @@ -34,7 +34,7 @@ type Exchange struct { rawbuf []byte respHeaderOff uint16 respHeaderLen uint16 - reqHdr httpraw.Header + reqHdr httpraw.HeaderV1 pathValues []PathValue // bodyRW is the reader handed to [httpraw.Form.ReadLimited], kept here so // boxing it into an io.Reader allocates nothing per request. @@ -171,13 +171,18 @@ func (exch *Exchange) Release() { // written to and used without modifying the staged response first line. // // Staging headers will write to this buffer so use mindfully. -// To access only the request header buffer portion use [httpraw.Header.BufferRaw] limited -// to [httpraw.Header.BufferParsed] as returned by [Exchange.RequestHeaderRaw]. +// To access only the request header buffer portion use [httpraw.HeaderV1.BufferRaw] limited +// to [httpraw.HeaderV1.BufferParsed] as returned by [Exchange.requestHeaderRaw]. // Writing to this section will not change the contents read by [Exchange.ReadBody]. // // In [Router] context, the size of this buffer is influenced directly by [RouterConfig] HeaderBufferSize fields. func (exch *Exchange) UnsafeRawBuffer() []byte { return exch.rawbuf } +// RequestHeaderV1Raw returns the parsed request header for access beyond the +// Request* methods, such as [httpraw.HeaderV1.ForEach]. Valid until the exchange +// is released, and writing to it corrupts the response. +func (exch *Exchange) RequestHeaderV1Raw() *httpraw.HeaderV1 { return &exch.reqHdr } + // StageHeader stages a response header field, written on the first // [Exchange.FlushHeader], [Exchange.WriteHeader] or [Exchange.WriteBody]. // Returns false and drops the field if the response buffer cannot fit it. @@ -478,13 +483,6 @@ func (exch *Exchange) MuxPattern() string { return exch.matchedPattern } -// RequestHeaderRaw returns the parsed request header for access beyond the -// Request* methods, such as [httpraw.Header.ForEach]. Valid until the exchange -// is released, and writing to it corrupts the response. -func (exch *Exchange) RequestHeaderRaw() *httpraw.Header { - return &exch.reqHdr -} - // RequestParseCookie parses the request's key header field into dst, i.e: // "Cookie". The caller owns dst and its buffer, so it may be reused between // requests. @@ -499,14 +497,14 @@ func (exch *Exchange) RequestParseCookie(dst *httpraw.Cookie, key string) error func (exch *Exchange) RequestContentType() []byte { // Folded: field names are case insensitive and HTTP/2 mandates lowercase, so // a proxy translating h2 to h1 sends "content-type", RFC 9110 5.1. - return exch.RequestHeaderRaw().GetFold("Content-Type") + return exch.RequestHeaderV1Raw().GetFold("Content-Type") } // RequestContentLength returns the body length declared by the request's // Content-Length field. An absent field is signalled with present=false and no error. -// See [httpraw.Header.ContentLength]. +// See [httpraw.HeaderV1.ContentLength]. func (exch *Exchange) RequestContentLength() (_ int64, present bool, _ error) { - return exch.RequestHeaderRaw().ContentLength() + return exch.RequestHeaderV1Raw().ContentLength() } // RequestParseForm parses "application/x-www-form-urlencoded" pairs into dst @@ -569,7 +567,7 @@ func (exch *Exchange) readBodyForm(dst *httpraw.Form) error { return nil // No declared encoding is no form, RFC 9110 8.3. } else if !httpraw.MediaTypeIs(contentType, "application/x-www-form-urlencoded") { return errNotFormEncoded - } else if exch.RequestHeaderRaw().GetFold("Transfer-Encoding") != nil { + } else if exch.RequestHeaderV1Raw().GetFold("Transfer-Encoding") != nil { // Chunked bodies are framed, so reading Content-Length bytes off the // wire would parse chunk sizes as form data. httpraw does not decode them. return errUnsupportedTransferCoding @@ -723,26 +721,26 @@ func (exch *Exchange) ReadMultiparts(dst []MultipartSink, buf []byte, newSink fu // RequestHeader returns the value of the first request header field matching // key, or nil if absent. Key matching is case sensitive. func (exch *Exchange) RequestHeader(key string) []byte { - header := exch.RequestHeaderRaw() + header := exch.RequestHeaderV1Raw() return header.Get(key) } // RequestTarget returns the request-target (URI) of the request line, i.e: -// "/search?q=go". See [httpraw.Header.RequestTarget]. +// "/search?q=go". See [httpraw.HeaderV1.RequestTarget]. func (exch *Exchange) RequestTarget() []byte { - return exch.RequestHeaderRaw().RequestTarget() + return exch.RequestHeaderV1Raw().RequestTarget() } // RequestPath returns the request-target (URI) up to the query string. This is // what the [Mux] matches on, i.e: "/search" for a request to "/search?q=go". func (exch *Exchange) RequestPath() []byte { - return exch.RequestHeaderRaw().RequestPath() + return exch.RequestHeaderV1Raw().RequestPath() } // RequestQuery returns the request's query string as it appears on the wire. -// Iterate it with [httpraw.NextQueryPair]. See [httpraw.Header.RequestQuery]. +// Iterate it with [httpraw.NextQueryPair]. See [httpraw.HeaderV1.RequestQuery]. func (exch *Exchange) RequestQuery() []byte { - return exch.RequestHeaderRaw().RequestQuery() + return exch.RequestHeaderV1Raw().RequestQuery() } // RequestQueryValue returns an undecoded view of the first query parameter @@ -835,11 +833,11 @@ func (exch *Exchange) RequestMethod() Method { // RequestMethod returns the request line's method as a []byte view, i.e: "GET". func (exch *Exchange) RequestMethodRaw() []byte { - return exch.RequestHeaderRaw().Method() + return exch.RequestHeaderV1Raw().Method() } // RequestConnectionClose returns true if the client asked for the connection to // be closed after this exchange with a "Connection: close" header field. func (exch *Exchange) RequestConnectionClose() bool { - return exch.RequestHeaderRaw().ConnectionClose() + return exch.RequestHeaderV1Raw().ConnectionClose() } diff --git a/http/httphi/exchange_test.go b/http/httphi/exchange_test.go index 38f8061..ed55202 100644 --- a/http/httphi/exchange_test.go +++ b/http/httphi/exchange_test.go @@ -1282,7 +1282,7 @@ func TestHandleBrowserSizedRequest(t *testing.T) { sm.Reset(1) sm.Handle("GET /echo", func(exch *Exchange) { gotMode = string(exch.RequestHeader("X-Mode")) - exch.RequestHeaderRaw().ForEach(func(key, value []byte) bool { + exch.RequestHeaderV1Raw().ForEach(func(key, value []byte) bool { fields++ return true }) diff --git a/http/httphi/httphi_fuzz_test.go b/http/httphi/httphi_fuzz_test.go index 99c2e06..012a0be 100644 --- a/http/httphi/httphi_fuzz_test.go +++ b/http/httphi/httphi_fuzz_test.go @@ -64,7 +64,7 @@ const ( // segSizes are the chunk sizes a request may be delivered in, index 0 meaning // "all at once". Splitting a request mid-CRLF or before a colon is what drives -// [httpraw.Header.TryParse]'s resumption path. Entries may be appended, never +// [httpraw.HeaderV1.TryParse]'s resumption path. Entries may be appended, never // changed: an existing index must keep splitting exactly as it does today. var segSizes = [16]int{0, 1, 2, 3, 5, 7, 11, 16, 23, 37, 64, 101, 173, 256, 509, 1024} @@ -152,7 +152,7 @@ func checkResponse(t *testing.T, written string) { if !strings.Contains(written, "\r\n\r\n") { t.Fatalf("header block never terminated: %q", written) } - var resp httpraw.Header + var resp httpraw.HeaderV1 const asResponse = true if err := resp.ParseBytes(asResponse, []byte(written)); err != nil { t.Fatalf("response does not parse back: %s in %q", err, written) diff --git a/http/httphi/mux.go b/http/httphi/mux.go index 3908990..bd38a53 100644 --- a/http/httphi/mux.go +++ b/http/httphi/mux.go @@ -81,6 +81,18 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error { } func (exch *Exchange) handleError(err error) { + if err == lneto.ErrUnsupported { + // httpraw refused a first line naming a version it does not speak, before + // spending the field loop on it. An empty protocol is a HTTP/0.9 + // simple-request, RFC 9112 3: a malformed 1.x request-line rather than a + // version there is any point naming back. + if len(exch.reqHdr.Protocol()) == 0 { + exch.WriteHeader(int(StatusBadRequest)) + } else { + exch.WriteHeader(int(StatusHTTPVersionNotSupported)) + } + return + } if err == httpraw.ErrHeaderTooMany || err == httpraw.ErrBufferExhausted || exch.reqHdr.BufferFree() == 0 { // The peer is owed an answer: no larger buffer is coming, so // say so instead of dropping the connection, RFC 6585 5. @@ -221,6 +233,7 @@ type MuxSlice struct { path string handler HandlerFunc pathVals int + spec int } } @@ -230,30 +243,50 @@ func (sm *MuxSlice) Reset(capacity int) { internal.SliceReuse(&sm._handlers, capacity) } -// LookupHandler returns the handler registered for request path, or nil if none matches. -// The first registration matching both method and uri wins. +// LookupHandler returns the handler registered for request path, or nil if none +// matches. The most specific matching registration wins, not the first, so the +// catch-all "/" may be registered alongside the endpoints it backs without +// shadowing them, as in http.ServeMux, see [patternSpecificity]. Registrations +// of equal specificity are resolved in registration order. // // Every method this package does not name is [MethUnknown], so a request with an // extension method matches a bare-path registration and any registration naming // an extension method, whichever it names. Tell PROPFIND from MKCOL inside the // handler with [Exchange.RequestMethodRaw]. func (sm *MuxSlice) LookupHandler(method Method, path []byte, dstPathVals []PathValue) (matched string, _ HandlerFunc) { - for _, endpoint := range sm._handlers { + best := -1 + bestSpec := 0 + for i, endpoint := range sm._handlers { if endpoint.method != MethUndefined && endpoint.method != method { continue + } else if best >= 0 && endpoint.spec <= bestSpec { + continue // Cannot beat the incumbent, so do not pay to match it. } // Method matches. A pattern ending in '/' is a wildcard despite binding no // values: the trailing slash is an anonymous "{...}", so it must go // through the matcher and not a literal compare, see [SetPathValues]. + var ok bool if isWildcardPattern(endpoint.path) { - if ok, _ := SetPathValues(dstPathVals, endpoint.path, path); ok { - return endpoint.path, endpoint.handler - } - } else if b2s(path) == endpoint.path { - return endpoint.path, endpoint.handler + // dstPathVals is scratch during the scan: a candidate that matches and + // is then beaten, or one that is beaten and clears on failure, would + // leave the winner's values wrong, so the winner is bound below. + ok, _ = SetPathValues(dstPathVals, endpoint.path, path) + } else { + ok = b2s(path) == endpoint.path + } + if ok { + best, bestSpec = i, endpoint.spec } } - return "", nil + if best < 0 { + return "", nil + } + winner := sm._handlers[best] + if isWildcardPattern(winner.path) { + clear(dstPathVals) // The scan may have bound more values than the winner does. + SetPathValues(dstPathVals, winner.path, path) + } + return winner.path, winner.handler } // MaxPathValues returns the maximum number of path values any endpoint could have. @@ -303,11 +336,51 @@ func (sm *MuxSlice) Handle(optMethodAndPath string, handler HandlerFunc) { } v := internal.SliceReclaim(&sm._handlers) v.pathVals = countPathValues(url) + v.spec = patternSpecificity(url) v.method = method v.path = url v.handler = handler } +// patternSpecificity scores how tightly pattern pins a path, letting +// [MuxSlice.LookupHandler] prefer the most specific match over the first one +// registered. A literal segment pins harder than a wildcard segment, and a +// pattern left open at the end ("/", "/files/", "/{p...}") pins less than one +// spent on the whole path, so "/cnt" outscores "/" and "/users/me" outscores +// "/users/{id}". Scoring at registration keeps lookup to an integer compare. +// +// The score is a total order over patterns, which the subset relation is not: +// neither of "/a/{x}/c" and "/a/b/{y}" is more specific than the other, and they +// tie here where http.ServeMux rejects the pair as conflicting. A tie is settled +// by registration order rather than by a panic. +func patternSpecificity(pattern string) (spec int) { + if len(pattern) == 0 || pattern[0] != '/' { + return 0 + } + pattern = pattern[1:] + for { + if len(pattern) == 0 { + return spec // Nothing after a slash: an anonymous "{...}" taking the rest. + } + segment, rest, more := strings.Cut(pattern, "/") + name, isMulti, isWildcard := pathWildcard(segment) + switch { + case isWildcard && name == "$": + return spec + 1 // Ends the path, so nothing is left open. + case isWildcard && isMulti: + return spec // Takes the remainder, pinning nothing more. + case isWildcard: + spec++ + default: + spec += 2 + } + if !more { + return spec + 1 // Spent on the last segment: the pattern is exact. + } + pattern = rest + } +} + // countPathValues is how many values pattern can bind, which is what sizes the // slice [SetPathValues] writes into. Only a named wildcard segment binds: "{$}" // marks the path's end, an anonymous "{...}" has no name to bind under, and a diff --git a/http/httphi/mux_test.go b/http/httphi/mux_test.go index 6351d69..0a55f21 100644 --- a/http/httphi/mux_test.go +++ b/http/httphi/mux_test.go @@ -363,6 +363,85 @@ func TestMuxSliceHandleAllowsExtensionMethod(t *testing.T) { } } +// A catch-all registered before the endpoints it sits above must not swallow +// them. "/" is a wildcard pattern: its trailing slash is an anonymous "{...}", +// so a purely first-match-wins scan hands every request to it and the specific +// registrations below become dead code, answered with the root page instead of +// their own body. Registering the site root first is the ordinary way to write +// a mux, so the more specific pattern has to win regardless of order, as in +// http.ServeMux. +func TestMuxSliceSpecificPatternBeatsEarlierCatchAll(t *testing.T) { + for _, test := range []struct { + name string + register []string + path string + want string + }{ + { + name: "root registered first", + register: []string{"/", "/hello", "/cnt", "/6"}, + path: "/cnt", + want: "/cnt", + }, { + name: "root registered last", + register: []string{"/hello", "/cnt", "/6", "/"}, + path: "/cnt", + want: "/cnt", + }, { + name: "root still serves the root path", + register: []string{"/", "/cnt"}, + path: "/", + want: "/", + }, { + name: "root still catches the unregistered", + register: []string{"/", "/cnt"}, + path: "/nowhere", + want: "/", + }, { + name: "subtree wildcard loses to its own literal", + register: []string{"/files/", "/files/index"}, + path: "/files/index", + want: "/files/index", + }, { + name: "subtree wildcard keeps the rest", + register: []string{"/files/", "/files/index"}, + path: "/files/a/b", + want: "/files/", + }, { + name: "longer literal prefix wins over shorter subtree", + register: []string{"/", "/files/", "/files/a/b"}, + path: "/files/a/b", + want: "/files/a/b", + }, { + name: "named wildcard loses to the literal it covers", + register: []string{"/users/{id}", "/users/me"}, + path: "/users/me", + want: "/users/me", + }, { + name: "named wildcard keeps everything else", + register: []string{"/users/{id}", "/users/me"}, + path: "/users/42", + want: "/users/{id}", + }, + } { + t.Run(test.name, func(t *testing.T) { + var sm MuxSlice + sm.Reset(len(test.register)) + for _, pattern := range test.register { + sm.Handle(pattern, func(ex *Exchange) { ex.WriteHeader(200) }) + } + pathVals := make([]PathValue, max(sm.MaxPathValues(), 0)) + got, handler := sm.LookupHandler(MethGet, []byte(test.path), pathVals) + if handler == nil { + t.Fatalf("%s matched no handler, want %q", test.path, test.want) + } + if got != test.want { + t.Errorf("%s matched pattern %q, want %q", test.path, got, test.want) + } + }) + } +} + // pathVals sizes the slice SetPathValues writes into, so it must count exactly // the wildcards that bind. Counting braces over-reports: "{$}" marks the path's // end, an anonymous "{...}" has no name, and a brace inside a literal segment is diff --git a/http/httpraw/cookie.go b/http/httpraw/cookie.go index 00c3717..6b22a27 100644 --- a/http/httpraw/cookie.go +++ b/http/httpraw/cookie.go @@ -4,7 +4,7 @@ import ( "bytes" ) -// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [Header] methods. +// Cookie implements cookie key-value parsing. Methods function similarly to eponymous [HeaderV1] methods. // Cookie represents a single-line Cookie header value in a HTTP header, much like the standard library Cookie. type Cookie struct { kv kvBuffer @@ -16,7 +16,7 @@ func (c *Cookie) EnableBufferGrowth(enableBufferGrowth bool) { c.kv.EnableBufferGrowth(enableBufferGrowth) } -// Reset functions very similarly to [Header.Reset]. Can be used for in-place cookie parsing. +// Reset functions very similarly to [HeaderV1.Reset]. Can be used for in-place cookie parsing. func (c *Cookie) Reset(buf []byte, capKV int) { c.kv.Reset(buf, capKV) } func (c *Cookie) valid() bool { diff --git a/http/httpraw/header.go b/http/httpraw/headerv1.go similarity index 83% rename from http/httpraw/header.go rename to http/httpraw/headerv1.go index 8c40377..6750981 100644 --- a/http/httpraw/header.go +++ b/http/httpraw/headerv1.go @@ -8,7 +8,8 @@ import ( const ( methodGet = "GET" - strHTTP11 = "HTTP/1.1" + strHTTP1 = "HTTP/1." + strHTTP11 = strHTTP1 + "1" strCRLF = "\r\n" headerCookie = "Cookie" headerConnection = "Connection" @@ -19,7 +20,7 @@ const ( // Flags is a bitset of signals gathered while parsing or building a header, // such as a status code having been set or the peer requesting connection -// close. See [Header.Flags]. +// close. See [HeaderV1.Flags]. type Flags uint16 const ( @@ -40,15 +41,15 @@ func (f Flags) HasAny(checkThese Flags) bool { return f&checkThese != 0 } -// Header implements "raw" HTTP header key-value parsing, validation and marshalling. +// HeaderV1 implements "raw" HTTP header key-value parsing, validation and marshalling. // // It does NOT implement: // - Normalization. // - Cookies (see [Cookie]). // - Special header optimizations. // - Content-Length validation and other special header field value validation. -type Header struct { - hbuf headerBuf +type HeaderV1 struct { + hbuf headerv1Buf // Request fields. method view @@ -62,18 +63,18 @@ type Header struct { } // Flags returns [Flags] to signal status code has been set, Connection:Close or other useful signals provided by flags. -func (h *Header) Flags() Flags { return h.hbuf.kv.flags } +func (h *HeaderV1) Flags() Flags { return h.hbuf.kv.flags } // ConfigBufferGrowth configures the memory the header may use. Setting -// outlives [Header.Reset]. Call before parsing/reading. +// outlives [HeaderV1.Reset]. Call before parsing/reading. // // enableBufferGrowth enables growing both the header buffer and the header key/value pair slice. -func (h *Header) ConfigBufferGrowth(enableBufferGrowth bool) { +func (h *HeaderV1) ConfigBufferGrowth(enableBufferGrowth bool) { h.hbuf.kv.EnableBufferGrowth(enableBufferGrowth) } // 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 { +func (h *HeaderV1) ParseBytes(asResponse bool, b []byte) error { h.Reset(nil, 0) err := h.hbuf.kv.ReadFromBytes(b) if err != nil { @@ -82,9 +83,9 @@ func (h *Header) ParseBytes(asResponse bool, b []byte) error { return h.parse(asResponse) } -// Parse parses accumulated data in-place with no copying. One can set HTTP header data buffer with [Header.Reset]. +// Parse parses accumulated data in-place with no copying. One can set HTTP header data buffer with [HeaderV1.Reset]. // It fails if HTTP data is incomplete. -func (h *Header) Parse(asResponse bool) error { +func (h *HeaderV1) Parse(asResponse bool) error { debuglog("http:parse:reset") h.Reset(h.hbuf.kv.buf, 0) debuglog("http:parse:start") @@ -93,7 +94,7 @@ func (h *Header) Parse(asResponse bool) error { // TryParse begins parsing or resumes parsing from a failed previous attempt from any of the Parse* methods. // As long as needMoreData returns true future calls to TryParse may succeed and the header is not done parsing. -// Users may call [Header.ForEach] in-between TryParse calls so as to validate values before header is completely parsed. +// Users may call [HeaderV1.ForEach] in-between TryParse calls so as to validate values before header is completely parsed. // // needMoreData := true // var err error @@ -107,7 +108,7 @@ func (h *Header) Parse(asResponse bool) error { // if err != nil { // return err // } -func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) { +func (h *HeaderV1) TryParse(asResponse bool) (needMoreData bool, err error) { flags := h.Flags() if flags.HasAny(flagDoneParsingHeader) { return false, errAlreadyParsed @@ -125,26 +126,26 @@ func (h *Header) TryParse(asResponse bool) (needMoreData bool, err error) { } // ParsingSuccess returns true if TryParse was successful, that is to say it returned needMoreData==false and err==nil. -func (h *Header) ParsingSuccess() bool { +func (h *HeaderV1) ParsingSuccess() bool { return h.Flags().HasAny(flagDoneParsingHeader) } // ReadFromLimited reads at most maxBytesToRead from reader and appends them to underlying buffer. -// Used to accumulate HTTP header for later parsing with [Header.TryParse]. +// Used to accumulate HTTP header for later parsing with [HeaderV1.TryParse]. // If read is successful (read length>0) and reader returns [io.EOF] then ReadFromLimited will return a nil error. -func (h *Header) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) { +func (h *HeaderV1) ReadFromLimited(r io.Reader, maxBytesToRead int) (int, error) { return h.hbuf.kv.ReadLimited(r, maxBytesToRead) } // ReadFromBytes appends argument buffer to underlying buffer. -// Used to accumulate HTTP header for later parsing with [Header.TryParse]. -func (h *Header) ReadFromBytes(b []byte) error { +// Used to accumulate HTTP header for later parsing with [HeaderV1.TryParse]. +func (h *HeaderV1) ReadFromBytes(b []byte) error { return h.hbuf.kv.ReadFromBytes(b) } // BufferReceived returns the amoung of bytes read during calls to Read* methods. // Returns 0 if buffer is invalid/mangled. -func (h *Header) BufferReceived() int { +func (h *HeaderV1) BufferReceived() int { if h.Flags().HasAny(flagMangledBuffer | flagOOMReached) { return 0 } @@ -154,7 +155,7 @@ func (h *Header) BufferReceived() int { // BufferParsed returns the amount of bytes parsed during a call to Parse* methods. // If the Parse* method completed without error then BufferParsed returns the header's length including the final "\r\n\r\n" text. // BufferParsed returns 0 if the buffer is invalid/mangled or if no header data has been parsed succesfully. -func (h *Header) BufferParsed() int { +func (h *HeaderV1) BufferParsed() int { if h.Flags().HasAny(flagMangledBuffer | flagOOMReached) { return 0 } @@ -162,56 +163,56 @@ func (h *Header) BufferParsed() int { } // BufferRaw returns the undeerlying buffer as stored currently in memory. -// The length of the returned buffer is the used portion. Capacity of returned slice is [Header.BufferCapacity]. -func (h *Header) BufferRaw() []byte { return h.hbuf.kv.BufferRaw() } +// The length of the returned buffer is the used portion. Capacity of returned slice is [HeaderV1.BufferCapacity]. +func (h *HeaderV1) BufferRaw() []byte { return h.hbuf.kv.BufferRaw() } // BufferUsed returns the raw memory used. // // BufferUsed + BufferFree == BufferCapacity -func (h *Header) BufferUsed() int { +func (h *HeaderV1) BufferUsed() int { return len(h.hbuf.kv.BufferRaw()) } // BufferFree returns amount of bytes free in underlying buffer. // // BufferUsed + BufferFree == BufferCapacity -func (h *Header) BufferFree() int { +func (h *HeaderV1) BufferFree() int { return h.hbuf.free() } // BufferCapacity returns the total capacity of the underlying buffer. // // BufferUsed + BufferFree == BufferCapacity -func (h *Header) BufferCapacity() int { +func (h *HeaderV1) BufferCapacity() int { return cap(h.hbuf.kv.BufferRaw()) } // ForEach iterates over header key-value field tuples. -func (h *Header) ForEach(cb func(key, value []byte) bool) { +func (h *HeaderV1) ForEach(cb func(key, value []byte) bool) { h.hbuf.kv.ForEach(cb) } // 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 +// can be used to avoid copying and growing buffers. Call [HeaderV1.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], 16); h.ParseBytes(httpHeader) // Tell header to use a pre-allocated buffer capacity. // h.Reset(httpHeader, 16); 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, numHeaderCapacity int) { +func (h *HeaderV1) Reset(buf []byte, numHeaderCapacity int) { const persistentFlags = flagNoBufferGrow debuglog("http:reset:hbuf") h.hbuf.reset(buf, numHeaderCapacity) if h.Flags().HasAny(flagNoBufferGrow) && h.BufferCapacity() < 32 { panic("small buffer and flagNoBufferGrow set") } - *h = Header{hbuf: h.hbuf} + *h = HeaderV1{hbuf: h.hbuf} debuglog("http:reset:done") } // 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) { +func (h *HeaderV1) Body() ([]byte, error) { debuglog("http:body") flags := h.Flags() if flags.HasAny(flagMangledBuffer) { @@ -222,16 +223,16 @@ func (h *Header) Body() ([]byte, error) { return nil, errUnparsed } -// SetBytes is equivalent to [Header.Set] but with a []byte value. Does not keep reference to value slice. +// SetBytes is equivalent to [HeaderV1.Set] but with a []byte value. Does not keep reference to value slice. // Calling SetBytes Mangles the buffer. -func (h *Header) SetBytes(key string, value []byte) { +func (h *HeaderV1) SetBytes(key string, value []byte) { h.Set(key, b2s(value)) } -// SetInt is equivalent to [Header.Set] but with an integer value i.e: Content-Length header key. +// SetInt is equivalent to [HeaderV1.Set] but with an integer value i.e: Content-Length header key. // base must be in the range 2..36 (as accepted by [strconv.AppendInt]); other bases are dropped. // SetInt formats the value directly into the header buffer without heap allocation. -func (h *Header) SetInt(key string, value int64, base int) { +func (h *HeaderV1) SetInt(key string, value int64, base int) { if base < 2 || base > 36 { return // strconv.AppendInt only supports base 2..36. } @@ -240,25 +241,25 @@ func (h *Header) SetInt(key string, value int64, base int) { // Set sets a key-value pair in the HTTP header. // Calling Set mangles the buffer. -func (h *Header) Set(key, value string) (enoughSpace bool) { +func (h *HeaderV1) Set(key, value string) (enoughSpace bool) { return h.hbuf.kv.Set(key, value) } -// 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 { +// Get gets the first exact-match value of a key found in the headers. Use [HeaderV1.ForEach] to find multiple values corresponding to same key. +func (h *HeaderV1) Get(key string) []byte { return h.hbuf.kv.Get(key) } // 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 +// Use [HeaderV1.Get] for exact match and [HeaderV1.ForEach] to find multiple values // corresponding to same key. -func (h *Header) GetFold(key string) []byte { +func (h *HeaderV1) GetFold(key string) []byte { return h.hbuf.kv.GetFold(key) } // NormalizeKeys normalizes all header keys. i.e: CONTENT-type -> Content-Type -func (h *Header) NormalizeKeys() { +func (h *HeaderV1) NormalizeKeys() { for i, kv := range h.hbuf.kv.kvs { if kv.isValidHeader() { NormalizeHeaderKey(h.hbuf.kv.AtKey(i)) @@ -268,7 +269,7 @@ func (h *Header) NormalizeKeys() { // ContentLength returns the body length declared by the Content-Length field. // If the field is not present then the returned bool is false. Will return error for invalid or non-integer value. -func (h *Header) ContentLength() (_ int64, present bool, _ error) { +func (h *HeaderV1) ContentLength() (_ int64, present bool, _ error) { value := h.GetFold(headerContentLength) if value == nil { return 0, false, nil @@ -283,34 +284,34 @@ func (h *Header) ContentLength() (_ int64, present bool, _ error) { } // Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer. -func (h *Header) Add(key, value string) { +func (h *HeaderV1) Add(key, value string) { h.hbuf.kv.appendPair(key, value) } // Method returns HTTP request method. -func (h *Header) Method() []byte { +func (h *HeaderV1) Method() []byte { return h.getNonEmptyValue(h.method) } // SetMethod sets the request header's method. -func (h *Header) SetMethod(method string) { +func (h *HeaderV1) SetMethod(method string) { h.method = h.hbuf.kv.reuseOrAppend(h.method, method) } // SetRequestTarget sets request-target (URI) for the first HTTP request line. -func (h *Header) SetRequestTarget(requestTarget string) { +func (h *HeaderV1) SetRequestTarget(requestTarget string) { h.requestTarget = h.hbuf.kv.reuseOrAppend(h.requestTarget, requestTarget) } // RequestTarget returns a view of the request-target (URI) of the first HTTP request line. // Called Request-URI in the obsolete RFC 2616, renamed request-target by RFC 9112. -func (h *Header) RequestTarget() []byte { +func (h *HeaderV1) RequestTarget() []byte { return h.getNonEmptyValue(h.requestTarget) } // RequestPath returns the request-target (URI) up to the query string, i.e: "/search" // for "/search?q=go". Returns the whole target if it contains no query string. -func (h *Header) RequestPath() []byte { +func (h *HeaderV1) RequestPath() []byte { target := h.RequestTarget() before, _, ok := bytes.Cut(target, []byte{'?'}) if !ok { @@ -322,7 +323,7 @@ func (h *Header) RequestPath() []byte { // RequestQuery returns the request-target (URI) query string as it appears on the // wire, percent-encoded and with '+' undecoded, i.e: "q=go" for "/search?q=go". // Returns nil if the target has no query string. Iterate it with [NextQueryPair]. -func (h *Header) RequestQuery() []byte { +func (h *HeaderV1) RequestQuery() []byte { target := h.RequestTarget() _, after, ok := bytes.Cut(target, []byte{'?'}) if !ok { @@ -332,17 +333,17 @@ func (h *Header) RequestQuery() []byte { } // Protocol returns the request header's HTTP protocol. Usually "HTTP/1.1". -func (h *Header) Protocol() []byte { +func (h *HeaderV1) Protocol() []byte { return h.getNonEmptyValue(h.proto) } // SetProtocol sets the request header's protocol. Usually "HTTP/1.1". -func (h *Header) SetProtocol(protocol string) { +func (h *HeaderV1) SetProtocol(protocol string) { h.proto = h.hbuf.kv.reuseOrAppend(h.proto, protocol) } // Status returns the response header's status code and status text. i.e: "200" "OK". -func (h *Header) Status() (code, statusText []byte) { +func (h *HeaderV1) Status() (code, statusText []byte) { if h.statusCode.len == 0 { return nil, nil } @@ -350,20 +351,20 @@ func (h *Header) Status() (code, statusText []byte) { } // SetStatus sets the response header's status code and status text. i.e: "200" "OK". -func (h *Header) SetStatus(code, statusText string) { +func (h *HeaderV1) SetStatus(code, statusText string) { h.hbuf.kv.flags |= FlagStatusSet h.statusCode = h.hbuf.kv.reuseOrAppend(h.statusCode, code) h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText) } -// SetStatusInt is identical to [Header.SetStatus] but performs integer to text conversion for status code. -func (h *Header) SetStatusInt(code int64, statusText string) { +// SetStatusInt is identical to [HeaderV1.SetStatus] but performs integer to text conversion for status code. +func (h *HeaderV1) SetStatusInt(code int64, statusText string) { h.hbuf.kv.flags |= FlagStatusSet h.statusCode = h.hbuf.kv.reuseOrAppendInt(h.statusCode, code, 10) h.statusText = h.hbuf.kv.reuseOrAppend(h.statusText, statusText) } -func (h *Header) getNonEmptyValue(s view) []byte { +func (h *HeaderV1) getNonEmptyValue(s view) []byte { if s.len == 0 { return nil // If empty then value is invalid, return nil. } @@ -371,7 +372,7 @@ func (h *Header) getNonEmptyValue(s view) []byte { } // AppendRequest appends the request header representation to the buffer and returns the result. -func (h *Header) AppendRequest(dst []byte) ([]byte, error) { +func (h *HeaderV1) AppendRequest(dst []byte) ([]byte, error) { proto := h.Protocol() if h.hbuf.kv.flags.HasAny(flagOOMReached) { return dst, ErrBufferExhausted @@ -401,7 +402,7 @@ func (h *Header) AppendRequest(dst []byte) ([]byte, error) { } // AppendResponse appends the response header representation to the buffer and returns the result. -func (h *Header) AppendResponse(dst []byte) ([]byte, error) { +func (h *HeaderV1) AppendResponse(dst []byte) ([]byte, error) { dst, err := h.AppendResponseNoHeaders(dst) if err != nil { return dst, err @@ -411,7 +412,7 @@ func (h *Header) AppendResponse(dst []byte) ([]byte, error) { } // AppendResponseNoHeaders appends the first line of the response containing protocol and status code/text: i.e: "HTTP/1.1 200 OK\r\n" -func (h *Header) AppendResponseNoHeaders(dst []byte) ([]byte, error) { +func (h *HeaderV1) AppendResponseNoHeaders(dst []byte) ([]byte, error) { proto := h.Protocol() if h.hbuf.kv.flags.HasAny(flagOOMReached) { return dst, ErrBufferExhausted @@ -433,7 +434,7 @@ func (h *Header) AppendResponseNoHeaders(dst []byte) ([]byte, error) { // 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 { +func (h *HeaderV1) AppendHeaders(dst []byte) []byte { for i, kv := range h.hbuf.kv.kvs { if kv.isValidHeader() { k, v := h.hbuf.kv.At(i) @@ -446,7 +447,7 @@ func (h *Header) AppendHeaders(dst []byte) []byte { // String returns the header's wire representation, as a request if it has a // request line and as a response otherwise. Returns the error text if neither // can be built. Allocates, so it is meant for debugging and logging only. -func (h *Header) String() string { +func (h *HeaderV1) String() string { buf, err := h.AppendRequest(nil) if err != nil { buf, err = h.AppendResponse(nil) diff --git a/http/httpraw/header_test.go b/http/httpraw/headerv1_test.go similarity index 98% rename from http/httpraw/header_test.go rename to http/httpraw/headerv1_test.go index 351cc1c..8948613 100644 --- a/http/httpraw/header_test.go +++ b/http/httpraw/headerv1_test.go @@ -44,7 +44,7 @@ func TestHeaderParseRequest(t *testing.T) { var buf bytes.Buffer req.Write(&buf) - var hdr Header + var hdr HeaderV1 msg := buf.Bytes() start := time.Now() @@ -128,7 +128,7 @@ func BenchmarkParseBytes(b *testing.B) { // allocating on every iteration. Declaring it inside the loop causes // two allocs per iteration: one for the headers slice (make in reset) // and one for the data buffer (append in readFromBytes). - var hdr Header + var hdr HeaderV1 b.StartTimer() for b.Loop() { @@ -169,7 +169,7 @@ func TestHeaderRequestPath(t *testing.T) { {uri: "/a/b/c?x=1&y=2", want: "/a/b/c"}, {uri: "/?q=go", want: "/"}, } { - var h Header + var h HeaderV1 err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n")) if err != nil { t.Fatal(err) @@ -196,7 +196,7 @@ func TestHeaderContentLength(t *testing.T) { {field: "Content-Length: 1 2", wantErr: errBadContentLength}, // Not a list. {field: "Content-Length: 9223372036854775808", wantErr: errBadContentLength}, } { - var h Header + var h HeaderV1 raw := "POST / HTTP/1.1\r\nHost: h\r\n" if test.field != "" { raw += test.field + "\r\n" @@ -234,7 +234,7 @@ func TestNextQueryPair(t *testing.T) { {uri: "/x?a%20b=c%20d", want: "a%20b=c%20d"}, // Raw, undecoded. {uri: "/x?a=b=c", want: "a=b=c"}, // Only first '=' splits. } { - var h Header + var h HeaderV1 err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n")) if err != nil { t.Fatal(err) @@ -397,7 +397,7 @@ func TestCopyDecodedPercentURLInPlace(t *testing.T) { } func TestHeaderSetOverwrite(t *testing.T) { - var h Header + var h HeaderV1 h.Reset(nil, defaultKVCap) h.SetMethod("GET") h.SetRequestTarget("/") @@ -420,7 +420,7 @@ func TestHeaderSetOverwrite(t *testing.T) { } func TestHeaderSetBytesEmptyValue(t *testing.T) { - var h Header + var h HeaderV1 h.Reset(nil, defaultKVCap) h.SetBytes("X-Empty", nil) if got := h.Get("X-Empty"); len(got) != 0 { @@ -440,7 +440,7 @@ func TestHeader_LargeBufferOverflow(t *testing.T) { "X-Canary: " + wantVal + "\r\n" + "\r\n" - var h Header + var h HeaderV1 err := h.ParseBytes(false, []byte(raw)) if err != nil { // Clean rejection of the oversized header is the intended behavior: @@ -458,7 +458,7 @@ func TestHeader_LargeBufferOverflow(t *testing.T) { // not ErrNeedMoreData (which makes a streaming parser wait forever). func TestHeader_ColonlessLineIsHardError(t *testing.T) { raw := "GET / HTTP/1.1\r\nBadHeaderNoColon\r\n\r\n" - var h Header + var h HeaderV1 err := h.ParseBytes(false, []byte(raw)) if err == nil { t.Fatal("want error on colonless header line, got nil") @@ -475,7 +475,7 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) { const part1 = "GET / HTTP/1.1\r\nHost" // split mid-key, before colon+newline const part2 = ": example.com\r\n\r\n" - var h Header + var h HeaderV1 h.Reset(nil, defaultKVCap) if err := h.ReadFromBytes([]byte(part1)); err != nil { t.Fatal(err) @@ -508,7 +508,7 @@ func TestHeader_SplitBeforeColonStillParses(t *testing.T) { func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) { const key, value = "K", "V" buf := make([]byte, 0, len(key)+len(value)) // exact cap, no slack. - var h Header + var h HeaderV1 h.Reset(buf, defaultKVCap) defer func() { if r := recover(); r != nil { @@ -525,7 +525,7 @@ func TestHeader_AppendHeaderExactCapNoPanic(t *testing.T) { // never panic. Panicking is unacceptable for this package. func TestHeader_AddFullBufferNoPanic(t *testing.T) { buf := make([]byte, 0, 40) // Small cap; enough for Reset (len 0) but not the field below. - var h Header + var h HeaderV1 h.Reset(buf, defaultKVCap) h.ConfigBufferGrowth(false) h.SetMethod("GET") @@ -560,7 +560,7 @@ func TestHeader_SetInt(t *testing.T) { {"hex", 255, 16, "ff"}, } { t.Run(tc.name, func(t *testing.T) { - var h Header + var h HeaderV1 h.Reset(nil, defaultKVCap) h.SetInt("Content-Length", tc.value, tc.base) if got := string(h.Get("Content-Length")); got != tc.want { @@ -572,7 +572,7 @@ func TestHeader_SetInt(t *testing.T) { // SetInt on an existing key must reuse the slot in place (single field, latest value). func TestHeader_SetIntOverwrite(t *testing.T) { - var h Header + var h HeaderV1 h.Reset(nil, defaultKVCap) h.SetMethod("GET") h.SetRequestTarget("/") @@ -596,7 +596,7 @@ func TestHeader_SetIntOverwrite(t *testing.T) { // SetInt must not heap-allocate: it must format directly into the header buffer. func TestHeader_SetIntNoAlloc(t *testing.T) { buf := make([]byte, 0, 256) - var h Header + var h HeaderV1 h.Reset(buf, defaultKVCap) h.ConfigBufferGrowth(false) h.Add("Content-Length", "0000000000000000000000") // pre-size a reusable slot. @@ -626,7 +626,7 @@ func TestHeader_FieldTableSizedFromBuffer(t *testing.T) { } raw.WriteString("X-Canary: " + wantVal + "\r\n\r\n") - var h Header + var h HeaderV1 h.Reset(make([]byte, 0, 8192), defaultKVCap) // Room for the block with plenty to spare. err := h.ParseBytes(false, []byte(raw.String())) if err != nil { @@ -649,7 +649,7 @@ func TestHeader_FieldTableFullIsReported(t *testing.T) { raw.WriteString(":v\r\n") } raw.WriteString("\r\n") - var h Header + var h HeaderV1 h.Reset(make([]byte, 0, 512), defaultKVCap) h.ConfigBufferGrowth(false) err := h.ParseBytes(false, []byte(raw.String())) diff --git a/http/httpraw/kvbuffer.go b/http/httpraw/kvbuffer.go index 01c2da2..0072db6 100644 --- a/http/httpraw/kvbuffer.go +++ b/http/httpraw/kvbuffer.go @@ -468,7 +468,7 @@ func (pair pairKV) isValid() bool { return pair.key.len > 0 || pair.value.len > 0 } -// isValidHeader is for the append-built [Header] store, where mustAppendSlice +// isValidHeader is for the append-built [HeaderV1] store, where mustAppendSlice // burns byte 0 so a zero offset means absent. Drops offset-0 pairs otherwise. func (pair pairKV) isValidHeader() bool { return pair.key.start > 0 } diff --git a/http/httpraw/parse.go b/http/httpraw/parsev1.go similarity index 83% rename from http/httpraw/parse.go rename to http/httpraw/parsev1.go index 4016fba..5c0202a 100644 --- a/http/httpraw/parse.go +++ b/http/httpraw/parsev1.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" + "github.com/soypat/lneto" "github.com/soypat/lneto/internal" ) @@ -48,7 +49,11 @@ var ( // returning the wrong bytes or panicking on a wrapped slice bound. const maxBufLen = 0xffff -type headerBuf struct { +func protoIsV1(s string) bool { + return s[:min(len(strHTTP1), len(s))] == strHTTP1 +} + +type headerv1Buf struct { kv kvBuffer // 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 @@ -61,7 +66,7 @@ type headerBuf struct { // reset sets the buffer data and discards all parsed data. The field table is // grown to match the new buffer's capacity and never shrinks, so a header // reused across requests settles on its largest buffer and stops allocating. -func (h *headerBuf) reset(buf []byte, numHeaderCapacity int) { +func (h *headerv1Buf) reset(buf []byte, numHeaderCapacity int) { h.kv.Reset(buf, numHeaderCapacity) h.off = 0 } @@ -80,7 +85,7 @@ type scannerState struct { initialized bool } -func (h *Header) parse(asResponse bool) (err error) { +func (h *HeaderV1) parse(asResponse bool) (err error) { debuglog("http:firstline:start") err = h.parseFirstLine(asResponse) if err != nil { @@ -93,7 +98,7 @@ func (h *Header) parse(asResponse bool) (err error) { return err } -func (h *Header) parseFirstLine(asResponse bool) (err error) { +func (h *HeaderV1) parseFirstLine(asResponse bool) (err error) { if len(h.hbuf.kv.buf) > maxBufLen { return errBufferTooLarge // Offsets would overflow uint16 tokint. } @@ -107,7 +112,7 @@ func (h *Header) parseFirstLine(asResponse bool) (err error) { return err } -func (h *Header) parseNextHeaders(flags Flags) error { +func (h *HeaderV1) parseNextHeaders(flags Flags) error { var ss scannerState h.hbuf.parseNextHeaders(&ss, flags) if ss.err != nil { @@ -118,9 +123,9 @@ func (h *Header) parseNextHeaders(flags Flags) error { return nil } -func (hb *headerBuf) free() int { return hb.kv.free() } +func (hb *headerv1Buf) free() int { return hb.kv.free() } -func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) { +func (hb *headerv1Buf) parseNextHeaders(ss *scannerState, flags Flags) { debuglog("http:nexthdr:loop") for kv := hb.next(ss); kv.isValidHeader(); kv = hb.next(ss) { if !hb.kv.canAddOneKV() { @@ -132,17 +137,17 @@ func (hb *headerBuf) parseNextHeaders(ss *scannerState, flags Flags) { debuglog("http:nexthdr:done") } -func (hb *headerBuf) offBuf() []byte { +func (hb *headerv1Buf) offBuf() []byte { return hb.kv.buf[hb.off:] } -func (hb *headerBuf) skipLeadingCRLF() { +func (hb *headerv1Buf) skipLeadingCRLF() { for hb.off < len(hb.kv.buf) && (hb.kv.buf[hb.off] == '\n' || hb.kv.buf[hb.off] == '\r') { hb.off++ } } -func (hb *headerBuf) scanLine() []byte { +func (hb *headerv1Buf) scanLine() []byte { buf := hb.scanUntilByte('\n') if len(buf) > 0 && buf[len(buf)-1] == '\r' { buf = buf[:len(buf)-1] // exclude carriage return. @@ -153,7 +158,7 @@ func (hb *headerBuf) scanLine() []byte { return buf } -func (hb *headerBuf) scanUntilByte(c byte) []byte { +func (hb *headerv1Buf) scanUntilByte(c byte) []byte { buf := hb.offBuf() idx := bytes.IndexByte(buf, c) if idx >= 0 { @@ -163,7 +168,7 @@ func (hb *headerBuf) scanUntilByte(c byte) []byte { return buf } -func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto view, flags Flags, err error) { +func (hb *headerv1Buf) parseFirstLineRequest(initFlags Flags) (method, uri, proto view, flags Flags, err error) { debuglog("http:req:scan") hb.off = 0 // Parsing first line resets offset. hb.skipLeadingCRLF() @@ -183,21 +188,30 @@ func (hb *headerBuf) parseFirstLineRequest(initFlags Flags) (method, uri, proto reqURIEnd += methodEnd + 1 uri = hb.kv.view(b[methodEnd+1 : reqURIEnd]) proto = hb.kv.view(b[reqURIEnd+1:]) // Skip space before protocol. - if b2s(b[reqURIEnd+1:]) != strHTTP11 { - flags |= flagNoHTTP11 + protoText := b2s(b[reqURIEnd+1:]) + if !protoIsV1(protoText) { + // Refused here rather than after the fields: the field loop is nearly + // all of the parse cost and none of it serves a version this type + // does not speak. proto is set so the caller can name it, i.e: 505. + method = hb.kv.view(b[:methodEnd]) + return method, uri, proto, flags | flagNoHTTP11, lneto.ErrUnsupported + } else if protoText != strHTTP11 { + flags |= flagNoHTTP11 // HTTP/1.0, which defaults to closing the connection. } } else if reqURIEnd == 0 { return method, uri, proto, flags, errEmptyURI } else { - // No version provided. - flags |= flagNoHTTP11 + // No version at all is a HTTP/0.9 simple-request, not a 1.x request-line, + // RFC 9112 3. proto stays empty, telling it apart from a named version. uri = hb.kv.view(b[methodEnd+1:]) + method = hb.kv.view(b[:methodEnd]) + return method, uri, proto, flags | flagNoHTTP11, lneto.ErrUnsupported } method = hb.kv.view(b[:methodEnd]) return method, uri, proto, flags, nil } -func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, statusText view, flags Flags, err error) { +func (hb *headerv1Buf) parseFirstLineResponse(initFlags Flags) (statusCode, statusText view, flags Flags, err error) { debuglog("http:resp:scan") hb.off = 0 // Parsing first line resets offset. hb.skipLeadingCRLF() @@ -216,7 +230,11 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, status if protoEnd < 0 { return statusCode, statusText, flags, ErrNeedMoreData } - if b2s(b[:protoEnd]) != strHTTP11 { + if !protoIsV1(b2s(b[:protoEnd])) { + // Refused before the fields, as on the request side: a response naming + // another version is not one this type can read. + return statusCode, statusText, flags | flagNoHTTP11, lneto.ErrUnsupported + } else if b2s(b[:protoEnd]) != strHTTP11 { flags |= flagNoHTTP11 } b = b[protoEnd+1:] // Advance past protocol and space. @@ -243,7 +261,7 @@ func (hb *headerBuf) parseFirstLineResponse(initFlags Flags) (statusCode, status return statusCode, statusText, flags, nil } -func (hb *headerBuf) next(ss *scannerState) pairKV { +func (hb *headerv1Buf) next(ss *scannerState) pairKV { if !ss.initialized { ss.nextColon = -1 ss.nextNewLine = -1 @@ -328,7 +346,7 @@ func (hb *headerBuf) next(ss *scannerState) pairKV { } // ConnectionClose returns true if 'Connection: close' header is set or if a invalid header was found. -func (h *Header) ConnectionClose() bool { +func (h *HeaderV1) ConnectionClose() bool { flags := h.Flags() closed := flags.HasAny(flagConnClose) || h.hasConnectionToken(strClose) || @@ -342,7 +360,7 @@ func (h *Header) ConnectionClose() bool { // hasConnectionToken reports whether the Connection field lists token, which // must be lowercase. The field name, its comma list and each token all compare // case insensitively, RFC 9110 5.1 and 7.6.1. -func (h *Header) hasConnectionToken(token string) bool { +func (h *HeaderV1) hasConnectionToken(token string) bool { value := h.GetFold(headerConnection) for len(value) > 0 { item := value diff --git a/http/httpraw/parse_test.go b/http/httpraw/parsev1_test.go similarity index 82% rename from http/httpraw/parse_test.go rename to http/httpraw/parsev1_test.go index de433d3..d4e2d59 100644 --- a/http/httpraw/parse_test.go +++ b/http/httpraw/parsev1_test.go @@ -2,14 +2,17 @@ package httpraw import ( "bytes" + "errors" "strings" "testing" + + "github.com/soypat/lneto" ) func TestTryParse_IncrementalRequest(t *testing.T) { // Full HTTP request split across multiple ReadFromBytes calls. full := "GET /index.html HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/html\r\n\r\nbody here" - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) // Feed data in small chunks to exercise incremental parsing. @@ -78,7 +81,7 @@ func TestTryParse_IncrementalRequest(t *testing.T) { func TestTryParse_IncrementalResponse(t *testing.T) { full := "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nServer: lneto\r\n\r\nhello" - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) chunks := splitInto(full, 8) @@ -130,7 +133,7 @@ func TestReadFromLimited(t *testing.T) { data := "GET / HTTP/1.1\r\nHost: test\r\n\r\n" r := strings.NewReader(data) - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) // Read in one shot. @@ -156,7 +159,7 @@ func TestReadFromLimited(t *testing.T) { } func TestReadFromLimited_MaxBytes(t *testing.T) { - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) // Zero maxBytesToRead should error. @@ -167,7 +170,7 @@ func TestReadFromLimited_MaxBytes(t *testing.T) { } func TestReadFromBytes_Empty(t *testing.T) { - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) err := hdr.ReadFromBytes(nil) @@ -177,7 +180,7 @@ func TestReadFromBytes_Empty(t *testing.T) { } func TestBufferFreeAndCapacity(t *testing.T) { - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 100), defaultKVCap) if hdr.BufferCapacity() != 100 { @@ -194,7 +197,7 @@ func TestBufferFreeAndCapacity(t *testing.T) { } func TestEnableBufferGrowth(t *testing.T) { - var hdr Header + var hdr HeaderV1 buf := make([]byte, 0, 64) hdr.Reset(buf, defaultKVCap) hdr.ConfigBufferGrowth(false) @@ -211,7 +214,7 @@ func TestEnableBufferGrowth(t *testing.T) { func TestHeader_Add(t *testing.T) { full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(false, []byte(full)) if err != nil { t.Fatal(err) @@ -238,7 +241,7 @@ func TestHeader_Add(t *testing.T) { func TestHeader_SetBytes(t *testing.T) { full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(false, []byte(full)) if err != nil { t.Fatal(err) @@ -254,7 +257,7 @@ func TestHeader_SetBytes(t *testing.T) { func TestConnectionClose(t *testing.T) { t.Run("HTTP11_NoConnectionHeader", func(t *testing.T) { full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 hdr.ParseBytes(false, []byte(full)) if hdr.ConnectionClose() { t.Error("HTTP/1.1 without Connection:close should not close") @@ -263,7 +266,7 @@ func TestConnectionClose(t *testing.T) { t.Run("ExplicitClose", func(t *testing.T) { full := "GET / HTTP/1.1\r\nConnection: close\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 hdr.ParseBytes(false, []byte(full)) if !hdr.ConnectionClose() { t.Error("Connection:close header should trigger close") @@ -272,7 +275,7 @@ func TestConnectionClose(t *testing.T) { t.Run("HTTP10_NoKeepAlive", func(t *testing.T) { full := "GET / HTTP/1.0\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 hdr.ParseBytes(false, []byte(full)) if !hdr.ConnectionClose() { t.Error("HTTP/1.0 without keep-alive should close") @@ -281,7 +284,7 @@ func TestConnectionClose(t *testing.T) { t.Run("HTTP10_KeepAlive", func(t *testing.T) { full := "GET / HTTP/1.0\r\nConnection: keep-alive\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 hdr.ParseBytes(false, []byte(full)) if hdr.ConnectionClose() { t.Error("HTTP/1.0 with keep-alive should not close") @@ -291,7 +294,7 @@ func TestConnectionClose(t *testing.T) { func TestTryParse_AlreadyParsed(t *testing.T) { full := "GET / HTTP/1.1\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 hdr.ParseBytes(false, []byte(full)) // Calling TryParse again should return error. @@ -303,7 +306,7 @@ func TestTryParse_AlreadyParsed(t *testing.T) { func TestParseResponse_BadStatusCode(t *testing.T) { full := "HTTP/1.1 abc Bad\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(true, []byte(full)) if err == nil { t.Fatal("expected error for non-numeric status code") @@ -370,7 +373,7 @@ func TestCookie_ForEach(t *testing.T) { func TestHeader_MultilineValue(t *testing.T) { // RFC 7230: obsolete line folding with \r\n followed by space/tab. full := "GET / HTTP/1.1\r\nX-Multi: line1\r\n\tline2\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(false, []byte(full)) if err != nil { t.Fatal(err) @@ -386,7 +389,7 @@ func TestHeader_MultilineValue(t *testing.T) { } func TestHeader_ResponseRoundTrip(t *testing.T) { - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) hdr.SetProtocol("HTTP/1.1") hdr.SetStatus("404", "Not Found") @@ -411,7 +414,7 @@ func TestHeader_ResponseRoundTrip(t *testing.T) { } // Parse back the generated response. - var hdr2 Header + var hdr2 HeaderV1 err = hdr2.ParseBytes(true, buf) if err != nil { t.Fatalf("re-parse response: %v", err) @@ -426,7 +429,7 @@ func TestHeader_ResponseRoundTrip(t *testing.T) { } func TestHeader_RequestRoundTrip(t *testing.T) { - var hdr Header + var hdr HeaderV1 hdr.Reset(make([]byte, 0, 256), defaultKVCap) hdr.SetProtocol("HTTP/1.1") hdr.SetMethod("POST") @@ -444,7 +447,7 @@ func TestHeader_RequestRoundTrip(t *testing.T) { } // Parse back the generated request. - var hdr2 Header + var hdr2 HeaderV1 err = hdr2.ParseBytes(false, buf) if err != nil { t.Fatalf("re-parse request: %v", err) @@ -463,7 +466,7 @@ func TestHeader_RequestRoundTrip(t *testing.T) { func TestParseResponse_StatusCodeOnly(t *testing.T) { // Response with status code but no status text. full := "HTTP/1.1 204\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(true, []byte(full)) if err != nil { t.Fatal(err) @@ -477,7 +480,7 @@ func TestParseResponse_StatusCodeOnly(t *testing.T) { func TestParseResponse_HTTP10(t *testing.T) { full := "HTTP/1.0 200 OK\r\nServer: old\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(true, []byte(full)) if err != nil { t.Fatal(err) @@ -492,12 +495,14 @@ func TestParseResponse_HTTP10(t *testing.T) { } func TestParseRequest_NoProtocol(t *testing.T) { - // HTTP/0.9 style: just method and URI, no version. + // HTTP/0.9 style: just method and URI, no version. Refused, but the + // request-line it did read stays readable so a caller can answer 400 and say + // what it saw. full := "GET /simple\r\nHost: test\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(false, []byte(full)) - if err != nil { - t.Fatal(err) + if !errors.Is(err, lneto.ErrUnsupported) { + t.Fatalf("want lneto.ErrUnsupported for a version-less request, got %v", err) } if string(hdr.Method()) != "GET" { t.Errorf("method = %q; want GET", hdr.Method()) @@ -505,6 +510,8 @@ func TestParseRequest_NoProtocol(t *testing.T) { if string(hdr.RequestTarget()) != "/simple" { t.Errorf("URI = %q; want /simple", hdr.RequestTarget()) } + // An empty protocol is what tells HTTP/0.9 apart from a named version, which + // is how [httphi] picks 400 over 505. if hdr.Protocol() != nil { t.Errorf("protocol should be nil for version-less request, got %q", hdr.Protocol()) } @@ -521,7 +528,7 @@ func TestCookie_QuotedValue(t *testing.T) { func TestParseRequest_InvalidHeaderSpaceBeforeColon(t *testing.T) { // RFC 7230 ยง3.2.4: No whitespace allowed between header name and colon. full := "GET / HTTP/1.1\r\nBad Header : value\r\n\r\n" - var hdr Header + var hdr HeaderV1 err := hdr.ParseBytes(false, []byte(full)) if err == nil { t.Fatal("expected error for space before colon in header name") @@ -567,7 +574,7 @@ func TestConnectionCloseFolded(t *testing.T) { {proto: "HTTP/1.0", field: "Host: h", wantClose: true}, } { t.Run(test.proto+" "+test.field, func(t *testing.T) { - var hdr Header + var hdr HeaderV1 full := "GET / " + test.proto + "\r\nHost: h\r\n" + test.field + "\r\n\r\n" if err := hdr.ParseBytes(false, []byte(full)); err != nil { t.Fatal(err) @@ -578,3 +585,63 @@ func TestConnectionCloseFolded(t *testing.T) { }) } } + +// HeaderV1 speaks HTTP/1.x and nothing else, so a request or response naming +// another version is refused on the first line. That skips the field loop, which +// is where nearly all the parse cost is, and refuses the h2c preface a modern +// client opens with before it is mistaken for a request. +func TestHeaderV1RejectsUnsupportedVersion(t *testing.T) { + for _, test := range []struct { + name string + raw string + asResponse bool + }{ + {name: "request http2", raw: "GET / HTTP/2.0\r\nHost: h\r\n\r\n"}, + {name: "request http3", raw: "GET / HTTP/3.0\r\nHost: h\r\n\r\n"}, + {name: "h2c preface", raw: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"}, + {name: "request http09 no version", raw: "GET /index.html\r\nHost: h\r\n\r\n"}, + {name: "request bogus proto", raw: "GET / BANANA\r\nHost: h\r\n\r\n"}, + {name: "response http2", raw: "HTTP/2.0 200 OK\r\nServer: s\r\n\r\n", asResponse: true}, + {name: "response http09", raw: "HTTP/0.9 200 OK\r\nServer: s\r\n\r\n", asResponse: true}, + } { + t.Run(test.name, func(t *testing.T) { + var h HeaderV1 + err := h.ParseBytes(test.asResponse, []byte(test.raw)) + if !errors.Is(err, lneto.ErrUnsupported) { + t.Fatalf("want lneto.ErrUnsupported, got %v", err) + } + // The field loop must not have run: refusing early is the point. + fields := 0 + h.ForEach(func(key, value []byte) bool { fields++; return true }) + if fields != 0 { + t.Errorf("want no fields parsed, got %d", fields) + } + if h.ParsingSuccess() { + t.Error("a refused header must not report a successful parse") + } + }) + } +} + +// Both HTTP/1 versions stay supported: only non-1.x is refused. +func TestHeaderV1AcceptsV1Versions(t *testing.T) { + for _, test := range []struct { + raw string + asResponse bool + }{ + {raw: "GET / HTTP/1.1\r\nHost: h\r\n\r\n"}, + {raw: "GET / HTTP/1.0\r\nHost: h\r\n\r\n"}, + {raw: "HTTP/1.1 200 OK\r\nServer: s\r\n\r\n", asResponse: true}, + {raw: "HTTP/1.0 200 OK\r\nServer: s\r\n\r\n", asResponse: true}, + } { + t.Run(test.raw[:12], func(t *testing.T) { + var h HeaderV1 + if err := h.ParseBytes(test.asResponse, []byte(test.raw)); err != nil { + t.Fatalf("want parsed, got %v", err) + } + if !h.ParsingSuccess() { + t.Error("want a successful parse") + } + }) + } +} diff --git a/internet/pcap/capture.go b/internet/pcap/capture.go index f55abdb..5c8b25e 100644 --- a/internet/pcap/capture.go +++ b/internet/pcap/capture.go @@ -35,7 +35,7 @@ var ( ) type PacketBreakdown struct { - hdr httpraw.Header + hdr httpraw.HeaderV1 dmsg dns.Message vld lneto.Validator // SubfieldLimit will limit the number of captured subfields to the value it has. diff --git a/internet/pcap/capture_test.go b/internet/pcap/capture_test.go index 4ee6bbd..707de5f 100644 --- a/internet/pcap/capture_test.go +++ b/internet/pcap/capture_test.go @@ -23,7 +23,7 @@ import ( const httpProtocol = "HTTP/1.1" func makeHttpPayload(body string) ([]byte, error) { - var hdr httpraw.Header + var hdr httpraw.HeaderV1 hdr.SetProtocol(httpProtocol) hdr.SetStatus("200", "OK") hdr.Set("Cookie", "ABC=123") diff --git a/x/xnet/xnet_fuzz_test.go b/x/xnet/xnet_fuzz_test.go index 9db5503..671da33 100644 --- a/x/xnet/xnet_fuzz_test.go +++ b/x/xnet/xnet_fuzz_test.go @@ -21,7 +21,7 @@ func FuzzStackPacketHTTP(f *testing.F) { const seed = 1 var buf [ethernet.MaxFrameLength]byte s1, s2, c1, c2 := newTCPStacks(f, seed, MTU) - var hdr httpraw.Header + var hdr httpraw.HeaderV1 err := s1.ListenTCP4(c1, 80) if err != nil { f.Fatal(err) @@ -129,7 +129,7 @@ func FuzzStackPacketHTTP(f *testing.F) { if n1 == 0 && n2 == 0 { if !closed { if c1.BufferedInput() > 0 { - var hdr httpraw.Header + var hdr httpraw.HeaderV1 n, _ := c1.Read(buf[:]) hdr.ReadFromBytes(buf[:n]) hdr.TryParse(false)