diff --git a/go.mod b/go.mod index 1b726174ee..3c1f360f37 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/miekg/dns v1.1.61 github.com/opencoff/go-sieve v0.2.1 github.com/powerman/check v1.7.0 - github.com/quic-go/quic-go v0.46.0 + github.com/quic-go/quic-go v0.47.0 golang.org/x/crypto v0.26.0 golang.org/x/net v0.28.0 golang.org/x/sys v0.23.0 @@ -38,7 +38,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/powerman/deepequal v0.1.0 // indirect - github.com/quic-go/qpack v0.4.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect diff --git a/go.sum b/go.sum index a741432001..119db08bc2 100644 --- a/go.sum +++ b/go.sum @@ -71,17 +71,18 @@ github.com/powerman/check v1.7.0 h1:PtRow0L73QgYSmXUBI5qe5MnDu3kowTAKQSHTbDH8Zs= github.com/powerman/check v1.7.0/go.mod h1:pCQPDCCVj1ksGj9OaMqFBjvet5Jg8TbMB3UJj8Nx98g= github.com/powerman/deepequal v0.1.0 h1:sVwtyTsBuYIvdbLR1O2wzRY63YgPqdGZmk/o80l+C/U= github.com/powerman/deepequal v0.1.0/go.mod h1:3k7aG/slufBhUANdN67o/UPg8i5YaiJ6FmibWX0cn04= -github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= -github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= -github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= +github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/vendor/github.com/quic-go/qpack/.golangci.yml b/vendor/github.com/quic-go/qpack/.golangci.yml index 4a91adc77a..e6b574e8cd 100644 --- a/vendor/github.com/quic-go/qpack/.golangci.yml +++ b/vendor/github.com/quic-go/qpack/.golangci.yml @@ -4,24 +4,20 @@ linters: disable-all: true enable: - asciicheck - - deadcode + - copyloopvar - exhaustive - - exportloopref - goconst - gofmt # redundant, since gofmt *should* be a no-op after gofumpt - gofumpt - goimports - gosimple + - govet - ineffassign - misspell - prealloc - - scopelint - staticcheck - stylecheck - - structcheck - unconvert - unparam - unused - - varcheck - - vet diff --git a/vendor/github.com/quic-go/qpack/README.md b/vendor/github.com/quic-go/qpack/README.md index 6ba4bad4a9..5bf1f77bd2 100644 --- a/vendor/github.com/quic-go/qpack/README.md +++ b/vendor/github.com/quic-go/qpack/README.md @@ -1,13 +1,14 @@ # QPACK -[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/marten-seemann/qpack) -[![Code Coverage](https://img.shields.io/codecov/c/github/marten-seemann/qpack/master.svg?style=flat-square)](https://codecov.io/gh/marten-seemann/qpack) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/quic-go/qpack)](https://pkg.go.dev/github.com/quic-go/qpack) +[![Code Coverage](https://img.shields.io/codecov/c/github/quic-go/qpack/master.svg?style=flat-square)](https://codecov.io/gh/quic-go/qpack) +[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/quic-go.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:quic-go) This is a minimal QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)) implementation in Go. It is minimal in the sense that it doesn't use the dynamic table at all, but just the static table and (Huffman encoded) string literals. Wherever possible, it reuses code from the [HPACK implementation in the Go standard library](/~https://github.com/golang/net/tree/master/http2/hpack). -It should be able to interoperate with other QPACK implemetations (both encoders and decoders), however it won't achieve a high compression efficiency. +It is interoperable with other QPACK implementations (both encoders and decoders), however it won't achieve a high compression efficiency. If you're interested in dynamic table support, please comment on [the issue](/~https://github.com/quic-go/qpack/issues/33). -## Running the interop tests +## Running the Interop Tests Install the [QPACK interop files](/~https://github.com/qpackers/qifs/) by running ```bash @@ -16,5 +17,5 @@ git submodule update --init --recursive Then run the tests: ```bash -ginkgo -r integrationtests +go test -v ./integrationtests/interop/ ``` diff --git a/vendor/github.com/quic-go/qpack/decoder.go b/vendor/github.com/quic-go/qpack/decoder.go index c900194133..88ea8ebbf2 100644 --- a/vendor/github.com/quic-go/qpack/decoder.go +++ b/vendor/github.com/quic-go/qpack/decoder.go @@ -196,9 +196,13 @@ func (d *Decoder) parseIndexedHeaderField() error { func (d *Decoder) parseLiteralHeaderField() error { buf := d.buf - if buf[0]&0x20 > 0 || buf[0]&0x10 == 0 { + if buf[0]&0x10 == 0 { return errNoDynamicTable } + // We don't need to check the value of the N-bit here. + // It's only relevant when re-encoding header fields, + // and determines whether the header field can be added to the dynamic table. + // Since we don't support the dynamic table, we can ignore it. index, buf, err := readVarInt(4, buf) if err != nil { return err diff --git a/vendor/github.com/quic-go/qpack/tools.go b/vendor/github.com/quic-go/qpack/tools.go deleted file mode 100644 index 8f71eea26e..0000000000 --- a/vendor/github.com/quic-go/qpack/tools.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build tools - -package qpack - -import _ "github.com/onsi/ginkgo/v2/ginkgo" diff --git a/vendor/github.com/quic-go/quic-go/connection.go b/vendor/github.com/quic-go/quic-go/connection.go index f4a5ca93ea..1411a77b73 100644 --- a/vendor/github.com/quic-go/quic-go/connection.go +++ b/vendor/github.com/quic-go/quic-go/connection.go @@ -864,7 +864,9 @@ func (s *connection) handlePacketImpl(rp receivedPacket) bool { if counter > 0 { p.buffer.Split() } - processed = s.handleShortHeaderPacket(p) + if wasProcessed := s.handleShortHeaderPacket(p); wasProcessed { + processed = true + } break } } @@ -1766,8 +1768,9 @@ func (s *connection) applyTransportParameters() { params := s.peerParams // Our local idle timeout will always be > 0. s.idleTimeout = s.config.MaxIdleTimeout - if s.idleTimeout > 0 && params.MaxIdleTimeout < s.idleTimeout { - s.idleTimeout = params.MaxIdleTimeout + // If the peer advertised an idle timeout, take the minimum of the values. + if params.MaxIdleTimeout > 0 { + s.idleTimeout = min(s.idleTimeout, params.MaxIdleTimeout) } s.keepAliveInterval = min(s.config.KeepAlivePeriod, min(s.idleTimeout/2, protocol.MaxKeepAliveInterval)) s.streamsMap.UpdateLimits(params) diff --git a/vendor/github.com/quic-go/quic-go/http3/conn.go b/vendor/github.com/quic-go/quic-go/http3/conn.go index 0fd9412f86..0f372b0dd6 100644 --- a/vendor/github.com/quic-go/quic-go/http3/conn.go +++ b/vendor/github.com/quic-go/quic-go/http3/conn.go @@ -3,8 +3,10 @@ package http3 import ( "context" "fmt" + "io" "log/slog" "net" + "net/http" "sync" "sync/atomic" "time" @@ -112,8 +114,32 @@ func (c *connection) openRequestStream( c.streams[str.StreamID()] = datagrams c.streamMx.Unlock() qstr := newStateTrackingStream(str, c, datagrams) - hstr := newStream(qstr, c, datagrams) - return newRequestStream(hstr, requestWriter, reqDone, c.decoder, disableCompression, maxHeaderBytes), nil + rsp := &http.Response{} + hstr := newStream(qstr, c, datagrams, func(r io.Reader, l uint64) error { + hdr, err := c.decodeTrailers(r, l, maxHeaderBytes) + if err != nil { + return err + } + rsp.Trailer = hdr + return nil + }) + return newRequestStream(hstr, requestWriter, reqDone, c.decoder, disableCompression, maxHeaderBytes, rsp), nil +} + +func (c *connection) decodeTrailers(r io.Reader, l, maxHeaderBytes uint64) (http.Header, error) { + if l > maxHeaderBytes { + return nil, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", l, maxHeaderBytes) + } + + b := make([]byte, l) + if _, err := io.ReadFull(r, b); err != nil { + return nil, err + } + fields, err := c.decoder.DecodeFull(b) + if err != nil { + return nil, err + } + return parseTrailers(fields) } func (c *connection) acceptStream(ctx context.Context) (quic.Stream, *datagrammer, error) { diff --git a/vendor/github.com/quic-go/quic-go/http3/headers.go b/vendor/github.com/quic-go/quic-go/http3/headers.go index d587efd447..05d13ff3cb 100644 --- a/vendor/github.com/quic-go/quic-go/http3/headers.go +++ b/vendor/github.com/quic-go/quic-go/http3/headers.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "net/textproto" "net/url" "strconv" "strings" @@ -22,12 +23,21 @@ type header struct { Status string // for Extended connect Protocol string - // parsed and deduplicated + // parsed and deduplicated. -1 if no Content-Length header is sent ContentLength int64 // all non-pseudo headers Headers http.Header } +// connection-specific header fields must not be sent on HTTP/3 +var invalidHeaderFields = [...]string{ + "connection", + "keep-alive", + "proxy-connection", + "transfer-encoding", + "upgrade", +} + func parseHeaders(headers []qpack.HeaderField, isRequest bool) (header, error) { hdr := header{Headers: make(http.Header, len(headers))} var readFirstRegularHeader, readContentLength bool @@ -73,6 +83,14 @@ func parseHeaders(headers []qpack.HeaderField, isRequest bool) (header, error) { if !httpguts.ValidHeaderFieldName(h.Name) { return header{}, fmt.Errorf("invalid header field name: %q", h.Name) } + for _, invalidField := range invalidHeaderFields { + if h.Name == invalidField { + return header{}, fmt.Errorf("invalid header field name: %q", h.Name) + } + } + if h.Name == "te" && h.Value != "trailers" { + return header{}, fmt.Errorf("invalid TE header field value: %q", h.Value) + } readFirstRegularHeader = true switch h.Name { case "content-length": @@ -89,6 +107,7 @@ func parseHeaders(headers []qpack.HeaderField, isRequest bool) (header, error) { } } } + hdr.ContentLength = -1 if len(contentLengthStr) > 0 { // use ParseUint instead of ParseInt, so that parsing fails on negative values cl, err := strconv.ParseUint(contentLengthStr, 10, 63) @@ -101,6 +120,17 @@ func parseHeaders(headers []qpack.HeaderField, isRequest bool) (header, error) { return hdr, nil } +func parseTrailers(headers []qpack.HeaderField) (http.Header, error) { + h := make(http.Header, len(headers)) + for _, field := range headers { + if field.IsPseudo() { + return nil, fmt.Errorf("http3: received pseudo header in trailer: %s", field.Name) + } + h.Add(field.Name, field.Value) + } + return h, nil +} + func requestFromHeaders(headerFields []qpack.HeaderField) (*http.Request, error) { hdr, err := parseHeaders(headerFields, true) if err != nil { @@ -178,25 +208,53 @@ func hostnameFromURL(url *url.URL) string { return "" } -func responseFromHeaders(headerFields []qpack.HeaderField) (*http.Response, error) { +// updateResponseFromHeaders sets up http.Response as an HTTP/3 response, +// using the decoded qpack header filed. +// It is only called for the HTTP header (and not the HTTP trailer). +// It takes an http.Response as an argument to allow the caller to set the trailer later on. +func updateResponseFromHeaders(rsp *http.Response, headerFields []qpack.HeaderField) error { hdr, err := parseHeaders(headerFields, false) if err != nil { - return nil, err + return err } if hdr.Status == "" { - return nil, errors.New("missing status field") - } - rsp := &http.Response{ - Proto: "HTTP/3.0", - ProtoMajor: 3, - Header: hdr.Headers, - ContentLength: hdr.ContentLength, + return errors.New("missing status field") } + rsp.Proto = "HTTP/3.0" + rsp.ProtoMajor = 3 + rsp.Header = hdr.Headers + processTrailers(rsp) + rsp.ContentLength = hdr.ContentLength + status, err := strconv.Atoi(hdr.Status) if err != nil { - return nil, fmt.Errorf("invalid status code: %w", err) + return fmt.Errorf("invalid status code: %w", err) } rsp.StatusCode = status rsp.Status = hdr.Status + " " + http.StatusText(status) - return rsp, nil + return nil +} + +// processTrailers initializes the rsp.Trailer map, and adds keys for every announced header value. +// The Trailer header is removed from the http.Response.Header map. +// It handles both duplicate as well as comma-separated values for the Trailer header. +// For example: +// +// Trailer: Trailer1, Trailer2 +// Trailer: Trailer3 +// +// Will result in a http.Response.Trailer map containing the keys "Trailer1", "Trailer2", "Trailer3". +func processTrailers(rsp *http.Response) { + rawTrailers, ok := rsp.Header["Trailer"] + if !ok { + return + } + + rsp.Trailer = make(http.Header) + for _, rawVal := range rawTrailers { + for _, val := range strings.Split(rawVal, ",") { + rsp.Trailer[http.CanonicalHeaderKey(textproto.TrimString(val))] = nil + } + } + delete(rsp.Header, "Trailer") } diff --git a/vendor/github.com/quic-go/quic-go/http3/http_stream.go b/vendor/github.com/quic-go/quic-go/http3/http_stream.go index 63bea0e18d..f02e778e87 100644 --- a/vendor/github.com/quic-go/quic-go/http3/http_stream.go +++ b/vendor/github.com/quic-go/quic-go/http3/http_stream.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "strconv" "github.com/quic-go/quic-go" "github.com/quic-go/quic-go/internal/protocol" @@ -49,16 +48,20 @@ type stream struct { bytesRemainingInFrame uint64 datagrams *datagrammer + + parseTrailer func(io.Reader, uint64) error + parsedTrailer bool } var _ Stream = &stream{} -func newStream(str quic.Stream, conn *connection, datagrams *datagrammer) *stream { +func newStream(str quic.Stream, conn *connection, datagrams *datagrammer, parseTrailer func(io.Reader, uint64) error) *stream { return &stream{ - Stream: str, - conn: conn, - buf: make([]byte, 16), - datagrams: datagrams, + Stream: str, + conn: conn, + buf: make([]byte, 16), + datagrams: datagrams, + parseTrailer: parseTrailer, } } @@ -75,12 +78,21 @@ func (s *stream) Read(b []byte) (int, error) { return 0, err } switch f := frame.(type) { - case *headersFrame: - // skip HEADERS frames - continue case *dataFrame: + if s.parsedTrailer { + return 0, errors.New("DATA frame received after trailers") + } s.bytesRemainingInFrame = f.Length break parseLoop + case *headersFrame: + if s.conn.perspective == protocol.PerspectiveServer { + continue + } + if s.parsedTrailer { + return 0, errors.New("additional HEADERS frame received after trailers") + } + s.parsedTrailer = true + return 0, s.parseTrailer(s.Stream, f.Length) default: s.conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "") // parseNextFrame skips over unknown frame types @@ -134,6 +146,7 @@ type requestStream struct { maxHeaderBytes uint64 reqDone chan<- struct{} disableCompression bool + response *http.Response sentRequest bool requestedGzip bool @@ -149,6 +162,7 @@ func newRequestStream( decoder *qpack.Decoder, disableCompression bool, maxHeaderBytes uint64, + rsp *http.Response, ) *requestStream { return &requestStream{ stream: str, @@ -157,6 +171,7 @@ func newRequestStream( decoder: decoder, disableCompression: disableCompression, maxHeaderBytes: maxHeaderBytes, + response: rsp, } } @@ -213,9 +228,8 @@ func (s *requestStream) ReadResponse() (*http.Response, error) { s.conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeGeneralProtocolError), "") return nil, fmt.Errorf("http3: failed to decode response headers: %w", err) } - - res, err := responseFromHeaders(hfs) - if err != nil { + res := s.response + if err := updateResponseFromHeaders(res, hfs); err != nil { s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeMessageError)) s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError)) return nil, fmt.Errorf("http3: invalid response: %w", err) @@ -223,26 +237,15 @@ func (s *requestStream) ReadResponse() (*http.Response, error) { // Check that the server doesn't send more data in DATA frames than indicated by the Content-Length header (if set). // See section 4.1.2 of RFC 9114. - contentLength := int64(-1) - if _, ok := res.Header["Content-Length"]; ok && res.ContentLength >= 0 { - contentLength = res.ContentLength - } - respBody := newResponseBody(s.stream, contentLength, s.reqDone) + respBody := newResponseBody(s.stream, res.ContentLength, s.reqDone) // Rules for when to set Content-Length are defined in https://tools.ietf.org/html/rfc7230#section-3.3.2. - _, hasTransferEncoding := res.Header["Transfer-Encoding"] isInformational := res.StatusCode >= 100 && res.StatusCode < 200 isNoContent := res.StatusCode == http.StatusNoContent isSuccessfulConnect := s.isConnect && res.StatusCode >= 200 && res.StatusCode < 300 - if !hasTransferEncoding && !isInformational && !isNoContent && !isSuccessfulConnect { - res.ContentLength = -1 - if clens, ok := res.Header["Content-Length"]; ok && len(clens) == 1 { - if clen64, err := strconv.ParseInt(clens[0], 10, 64); err == nil { - res.ContentLength = clen64 - } - } + if (isInformational || isNoContent || isSuccessfulConnect) && res.ContentLength == -1 { + res.ContentLength = 0 } - if s.requestedGzip && res.Header.Get("Content-Encoding") == "gzip" { res.Header.Del("Content-Encoding") res.Header.Del("Content-Length") diff --git a/vendor/github.com/quic-go/quic-go/http3/response_writer.go b/vendor/github.com/quic-go/quic-go/http3/response_writer.go index 8638ec5777..b8b68120cb 100644 --- a/vendor/github.com/quic-go/quic-go/http3/response_writer.go +++ b/vendor/github.com/quic-go/quic-go/http3/response_writer.go @@ -5,11 +5,13 @@ import ( "fmt" "log/slog" "net/http" + "net/textproto" "strconv" "strings" "time" "github.com/quic-go/qpack" + "golang.org/x/net/http/httpguts" ) // The HTTPStreamer allows taking over a HTTP/3 stream. The interface is implemented the http.Response.Body. @@ -28,10 +30,11 @@ const maxSmallResponseSize = 4096 type responseWriter struct { str *stream - conn Connection - header http.Header - buf []byte - status int // status code passed to WriteHeader + conn Connection + header http.Header + trailers map[string]struct{} + buf []byte + status int // status code passed to WriteHeader // for responses smaller than maxSmallResponseSize, we buffer calls to Write, // and automatically add the Content-Length header @@ -42,6 +45,7 @@ type responseWriter struct { headerComplete bool // set once WriteHeader is called with a status code >= 200 headerWritten bool // set once the response header has been serialized to the stream isHead bool + trailerWritten bool // set once the response trailers has been serialized to the stream hijacked bool // set on HTTPStream is called @@ -117,11 +121,9 @@ func (w *responseWriter) sniffContentType(p []byte) { // We can't use `w.header.Get` here since if the Content-Type was set to nil, we shouldn't do sniffing. _, haveType := w.header["Content-Type"] - // If the Transfer-Encoding or Content-Encoding was set and is non-blank, - // we shouldn't sniff the body. - hasTE := w.header.Get("Transfer-Encoding") != "" + // If the Content-Encoding was set and is non-blank, we shouldn't sniff the body. hasCE := w.header.Get("Content-Encoding") != "" - if !hasCE && !haveType && !hasTE && len(p) > 0 { + if !hasCE && !haveType && len(p) > 0 { w.header.Set("Content-Type", http.DetectContentType(p)) } } @@ -200,7 +202,26 @@ func (w *responseWriter) writeHeader(status int) error { return err } + // Handle trailer fields + if vals, ok := w.header["Trailer"]; ok { + for _, val := range vals { + for _, trailer := range strings.Split(val, ",") { + // We need to convert to the canonical header key value here because this will be called when using + // headers.Add or headers.Set. + trailer = textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(trailer)) + w.declareTrailer(trailer) + } + } + } + for k, v := range w.header { + if _, excluded := w.trailers[k]; excluded { + continue + } + // Ignore "Trailer:" prefixed headers + if strings.HasPrefix(k, http.TrailerPrefix) { + continue + } for index := range v { if err := enc.WriteField(qpack.HeaderField{Name: strings.ToLower(k), Value: v[index]}); err != nil { return err @@ -224,6 +245,15 @@ func (w *responseWriter) FlushError() error { return err } +func (w *responseWriter) flushTrailers() { + if w.trailerWritten { + return + } + if err := w.writeTrailers(); err != nil { + w.logger.Debug("could not write trailers", "error", err) + } +} + func (w *responseWriter) Flush() { if err := w.FlushError(); err != nil { if w.logger != nil { @@ -232,6 +262,69 @@ func (w *responseWriter) Flush() { } } +// declareTrailer adds a trailer to the trailer list, while also validating that the trailer has a +// valid name. +func (w *responseWriter) declareTrailer(k string) { + if !httpguts.ValidTrailerHeader(k) { + // Forbidden by RFC 9110, section 6.5.1. + w.logger.Debug("ignoring invalid trailer", slog.String("header", k)) + return + } + if w.trailers == nil { + w.trailers = make(map[string]struct{}) + } + w.trailers[k] = struct{}{} +} + +// hasNonEmptyTrailers checks to see if there are any trailers with an actual +// value set. This is possible by adding trailers to the "Trailers" header +// but never actually setting those names as trailers in the course of handling +// the request. In that case, this check may save us some allocations. +func (w *responseWriter) hasNonEmptyTrailers() bool { + for trailer := range w.trailers { + if _, ok := w.header[trailer]; ok { + return true + } + } + return false +} + +// writeTrailers will write trailers to the stream if there are any. +func (w *responseWriter) writeTrailers() error { + // promote headers added via "Trailer:" convention as trailers, these can be added after + // streaming the status/headers have been written. + for k := range w.header { + // Handle "Trailer:" prefix + if strings.HasPrefix(k, http.TrailerPrefix) { + w.declareTrailer(k) + } + } + + if !w.hasNonEmptyTrailers() { + return nil + } + + var b bytes.Buffer + enc := qpack.NewEncoder(&b) + for trailer := range w.trailers { + trailerName := strings.ToLower(strings.TrimPrefix(trailer, http.TrailerPrefix)) + if vals, ok := w.header[trailer]; ok { + for _, val := range vals { + if err := enc.WriteField(qpack.HeaderField{Name: trailerName, Value: val}); err != nil { + return err + } + } + } + } + + buf := make([]byte, 0, frameHeaderLen+b.Len()) + buf = (&headersFrame{Length: uint64(b.Len())}).Append(buf) + buf = append(buf, b.Bytes()...) + _, err := w.str.writeUnframed(buf) + w.trailerWritten = true + return err +} + func (w *responseWriter) HTTPStream() Stream { w.hijacked = true w.Flush() diff --git a/vendor/github.com/quic-go/quic-go/http3/server.go b/vendor/github.com/quic-go/quic-go/http3/server.go index 9e7cd644fc..9f285b6e73 100644 --- a/vendor/github.com/quic-go/quic-go/http3/server.go +++ b/vendor/github.com/quic-go/quic-go/http3/server.go @@ -571,7 +571,7 @@ func (s *Server) handleRequest(conn *connection, str quic.Stream, datagrams *dat if _, ok := req.Header["Content-Length"]; ok && req.ContentLength >= 0 { contentLength = req.ContentLength } - hstr := newStream(str, conn, datagrams) + hstr := newStream(str, conn, datagrams, nil) body := newRequestBody(hstr, contentLength, conn.Context(), conn.ReceivedSettings(), conn.Settings) req.Body = body @@ -625,6 +625,7 @@ func (s *Server) handleRequest(conn *connection, str quic.Stream, datagrams *dat } } r.Flush() + r.flushTrailers() } // abort the stream when there is a panic diff --git a/vendor/github.com/quic-go/quic-go/interface.go b/vendor/github.com/quic-go/quic-go/interface.go index cec92d6de6..2071b596f7 100644 --- a/vendor/github.com/quic-go/quic-go/interface.go +++ b/vendor/github.com/quic-go/quic-go/interface.go @@ -19,10 +19,6 @@ type StreamID = protocol.StreamID // A Version is a QUIC version number. type Version = protocol.Version -// A VersionNumber is a QUIC version number. -// Deprecated: VersionNumber was renamed to Version. -type VersionNumber = Version - const ( // Version1 is RFC 9000 Version1 = protocol.Version1 diff --git a/vendor/github.com/quic-go/quic-go/internal/ackhandler/sent_packet_handler.go b/vendor/github.com/quic-go/quic-go/internal/ackhandler/sent_packet_handler.go index 7a30f7ed74..b84f0dcbbc 100644 --- a/vendor/github.com/quic-go/quic-go/internal/ackhandler/sent_packet_handler.go +++ b/vendor/github.com/quic-go/quic-go/internal/ackhandler/sent_packet_handler.go @@ -756,7 +756,7 @@ func (h *sentPacketHandler) PeekPacketNumber(encLevel protocol.EncryptionLevel) pnSpace := h.getPacketNumberSpace(encLevel) pn := pnSpace.pns.Peek() // See section 17.1 of RFC 9000. - return pn, protocol.GetPacketNumberLengthForHeader(pn, pnSpace.largestAcked) + return pn, protocol.PacketNumberLengthForHeader(pn, pnSpace.largestAcked) } func (h *sentPacketHandler) PopPacketNumber(encLevel protocol.EncryptionLevel) protocol.PacketNumber { diff --git a/vendor/github.com/quic-go/quic-go/internal/handshake/crypto_setup.go b/vendor/github.com/quic-go/quic-go/internal/handshake/crypto_setup.go index 0fb75dc8a8..c8e6cb33e5 100644 --- a/vendor/github.com/quic-go/quic-go/internal/handshake/crypto_setup.go +++ b/vendor/github.com/quic-go/quic-go/internal/handshake/crypto_setup.go @@ -229,6 +229,9 @@ func (h *cryptoSetup) handleMessage(data []byte, encLevel protocol.EncryptionLev } func (h *cryptoSetup) handleEvent(ev tls.QUICEvent) (done bool, err error) { + //nolint:exhaustive + // Go 1.23 added new 0-RTT events, see /~https://github.com/quic-go/quic-go/issues/4272. + // We will start using these events when dropping support for Go 1.22. switch ev.Kind { case tls.QUICNoEvent: return true, nil diff --git a/vendor/github.com/quic-go/quic-go/internal/protocol/packet_number.go b/vendor/github.com/quic-go/quic-go/internal/protocol/packet_number.go index bd34016195..9422db9245 100644 --- a/vendor/github.com/quic-go/quic-go/internal/protocol/packet_number.go +++ b/vendor/github.com/quic-go/quic-go/internal/protocol/packet_number.go @@ -21,58 +21,36 @@ const ( PacketNumberLen4 PacketNumberLen = 4 ) -// DecodePacketNumber calculates the packet number based on the received packet number, its length and the last seen packet number -func DecodePacketNumber( - packetNumberLength PacketNumberLen, - lastPacketNumber PacketNumber, - wirePacketNumber PacketNumber, -) PacketNumber { - var epochDelta PacketNumber - switch packetNumberLength { - case PacketNumberLen1: - epochDelta = PacketNumber(1) << 8 - case PacketNumberLen2: - epochDelta = PacketNumber(1) << 16 - case PacketNumberLen3: - epochDelta = PacketNumber(1) << 24 - case PacketNumberLen4: - epochDelta = PacketNumber(1) << 32 +// DecodePacketNumber calculates the packet number based its length and the last seen packet number +// This function is taken from https://www.rfc-editor.org/rfc/rfc9000.html#section-a.3. +func DecodePacketNumber(length PacketNumberLen, largest PacketNumber, truncated PacketNumber) PacketNumber { + expected := largest + 1 + win := PacketNumber(1 << (length * 8)) + hwin := win / 2 + mask := win - 1 + candidate := (expected & ^mask) | truncated + if candidate <= expected-hwin && candidate < 1<<62-win { + return candidate + win } - epoch := lastPacketNumber & ^(epochDelta - 1) - var prevEpochBegin PacketNumber - if epoch > epochDelta { - prevEpochBegin = epoch - epochDelta + if candidate > expected+hwin && candidate >= win { + return candidate - win } - nextEpochBegin := epoch + epochDelta - return closestTo( - lastPacketNumber+1, - epoch+wirePacketNumber, - closestTo(lastPacketNumber+1, prevEpochBegin+wirePacketNumber, nextEpochBegin+wirePacketNumber), - ) + return candidate } -func closestTo(target, a, b PacketNumber) PacketNumber { - if delta(target, a) < delta(target, b) { - return a - } - return b -} - -func delta(a, b PacketNumber) PacketNumber { - if a < b { - return b - a - } - return a - b -} - -// GetPacketNumberLengthForHeader gets the length of the packet number for the public header +// PacketNumberLengthForHeader gets the length of the packet number for the public header // it never chooses a PacketNumberLen of 1 byte, since this is too short under certain circumstances -func GetPacketNumberLengthForHeader(packetNumber, leastUnacked PacketNumber) PacketNumberLen { - diff := uint64(packetNumber - leastUnacked) - if diff < (1 << (16 - 1)) { +func PacketNumberLengthForHeader(pn, largestAcked PacketNumber) PacketNumberLen { + var numUnacked PacketNumber + if largestAcked == InvalidPacketNumber { + numUnacked = pn + 1 + } else { + numUnacked = pn - largestAcked + } + if numUnacked < 1<<(16-1) { return PacketNumberLen2 } - if diff < (1 << (24 - 1)) { + if numUnacked < 1<<(24-1) { return PacketNumberLen3 } return PacketNumberLen4 diff --git a/vendor/github.com/quic-go/quic-go/logging/interface.go b/vendor/github.com/quic-go/quic-go/logging/interface.go index 254911bd78..1f8edb92c6 100644 --- a/vendor/github.com/quic-go/quic-go/logging/interface.go +++ b/vendor/github.com/quic-go/quic-go/logging/interface.go @@ -36,9 +36,6 @@ type ( StreamNum = protocol.StreamNum // The StreamType is the type of the stream (unidirectional or bidirectional). StreamType = protocol.StreamType - // The VersionNumber is the QUIC version. - // Deprecated: use Version instead. - VersionNumber = protocol.Version // The Version is the QUIC version. Version = protocol.Version diff --git a/vendor/github.com/quic-go/quic-go/oss-fuzz.sh b/vendor/github.com/quic-go/quic-go/oss-fuzz.sh index 22a577fe16..92a57a2ccd 100644 --- a/vendor/github.com/quic-go/quic-go/oss-fuzz.sh +++ b/vendor/github.com/quic-go/quic-go/oss-fuzz.sh @@ -3,12 +3,12 @@ # Install Go manually, since oss-fuzz ships with an outdated Go version. # See /~https://github.com/google/oss-fuzz/pull/10643. export CXX="${CXX} -lresolv" # required by Go 1.20 -wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz \ +wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz \ && mkdir temp-go \ && rm -rf /root/.go/* \ - && tar -C temp-go/ -xzf go1.22.0.linux-amd64.tar.gz \ + && tar -C temp-go/ -xzf go1.23.0.linux-amd64.tar.gz \ && mv temp-go/go/* /root/.go/ \ - && rm -rf temp-go go1.22.0.linux-amd64.tar.gz + && rm -rf temp-go go1.23.0.linux-amd64.tar.gz ( # fuzz qpack diff --git a/vendor/modules.txt b/vendor/modules.txt index 2947d0b876..c4bb10bc9c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -101,11 +101,11 @@ github.com/powerman/check # github.com/powerman/deepequal v0.1.0 ## explicit; go 1.16 github.com/powerman/deepequal -# github.com/quic-go/qpack v0.4.0 -## explicit; go 1.18 +# github.com/quic-go/qpack v0.5.1 +## explicit; go 1.22 github.com/quic-go/qpack -# github.com/quic-go/quic-go v0.46.0 -## explicit; go 1.21 +# github.com/quic-go/quic-go v0.47.0 +## explicit; go 1.22 github.com/quic-go/quic-go github.com/quic-go/quic-go/http3 github.com/quic-go/quic-go/internal/ackhandler