Bump github.com/quic-go/quic-go from 0.46.0 to 0.47.0

Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.46.0 to 0.47.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Changelog](https://github.com/quic-go/quic-go/blob/master/Changelog.md)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.46.0...v0.47.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot] 2024-09-09 03:58:11 +00:00 committed by GitHub
parent 7447fc4a0e
commit 0ce2c48d5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 291 additions and 136 deletions

4
go.mod
View file

@ -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

11
go.sum
View file

@ -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=

View file

@ -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

View file

@ -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/
```

View file

@ -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

View file

@ -1,5 +0,0 @@
//go:build tools
package qpack
import _ "github.com/onsi/ginkgo/v2/ginkgo"

View file

@ -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)

View file

@ -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) {

View file

@ -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")
}

View file

@ -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")

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

8
vendor/modules.txt vendored
View file

@ -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