Update quic-go

This commit is contained in:
Frank Denis 2024-04-27 22:33:56 +02:00
parent f8ce22d9b9
commit 34a1f2ebf5
53 changed files with 1608 additions and 1312 deletions

2
go.mod
View File

@ -20,7 +20,7 @@ require (
github.com/miekg/dns v1.1.59
github.com/opencoff/go-sieve v0.2.1
github.com/powerman/check v1.7.0
github.com/quic-go/quic-go v0.42.0
github.com/quic-go/quic-go v0.43.0
golang.org/x/crypto v0.22.0
golang.org/x/net v0.24.0
golang.org/x/sys v0.19.0

4
go.sum
View File

@ -73,8 +73,8 @@ github.com/powerman/deepequal v0.1.0 h1:sVwtyTsBuYIvdbLR1O2wzRY63YgPqdGZmk/o80l+
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.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/quic-go/quic-go v0.43.0 h1:sjtsTKWX0dsHpuMJvLxGqoQdtgJnbAPWY+W+5vjYW/g=
github.com/quic-go/quic-go v0.43.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
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=

View File

@ -5,11 +5,21 @@ linters-settings:
misspell:
ignore-words:
- ect
depguard:
rules:
quicvarint:
list-mode: strict
files:
- "**/github.com/quic-go/quic-go/quicvarint/*"
- "!$test"
allow:
- $gostd
linters:
disable-all: true
enable:
- asciicheck
- depguard
- exhaustive
- exportloopref
- goimports

View File

@ -2,11 +2,12 @@
<img src="docs/quic.png" width=303 height=124>
[![Documentation](https://img.shields.io/badge/docs-quic--go.net-red?style=flat)](https://quic-go.net/docs/)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/quic-go/quic-go)](https://pkg.go.dev/github.com/quic-go/quic-go)
[![Code Coverage](https://img.shields.io/codecov/c/github/quic-go/quic-go/master.svg?style=flat-square)](https://codecov.io/gh/quic-go/quic-go/)
[![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)
quic-go is an implementation of the QUIC protocol ([RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000), [RFC 9001](https://datatracker.ietf.org/doc/html/rfc9001), [RFC 9002](https://datatracker.ietf.org/doc/html/rfc9002)) in Go. It has support for HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)).
quic-go is an implementation of the QUIC protocol ([RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000), [RFC 9001](https://datatracker.ietf.org/doc/html/rfc9001), [RFC 9002](https://datatracker.ietf.org/doc/html/rfc9002)) in Go. It has support for HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)) and HTTP Datagrams ([RFC 9297](https://datatracker.ietf.org/doc/html/rfc9297)).
In addition to these base RFCs, it also implements the following RFCs:
* Unreliable Datagram Extension ([RFC 9221](https://datatracker.ietf.org/doc/html/rfc9221))
@ -16,207 +17,7 @@ In addition to these base RFCs, it also implements the following RFCs:
Support for WebTransport over HTTP/3 ([draft-ietf-webtrans-http3](https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/)) is implemented in [webtransport-go](https://github.com/quic-go/webtransport-go).
## Using QUIC
### Running a Server
The central entry point is the `quic.Transport`. A transport manages QUIC connections running on a single UDP socket. Since QUIC uses Connection IDs, it can demultiplex a listener (accepting incoming connections) and an arbitrary number of outgoing QUIC connections on the same UDP socket.
```go
udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: 1234})
// ... error handling
tr := quic.Transport{
Conn: udpConn,
}
ln, err := tr.Listen(tlsConf, quicConf)
// ... error handling
go func() {
for {
conn, err := ln.Accept()
// ... error handling
// handle the connection, usually in a new Go routine
}
}()
```
The listener `ln` can now be used to accept incoming QUIC connections by (repeatedly) calling the `Accept` method (see below for more information on the `quic.Connection`).
As a shortcut, `quic.Listen` and `quic.ListenAddr` can be used without explicitly initializing a `quic.Transport`:
```
ln, err := quic.Listen(udpConn, tlsConf, quicConf)
```
When using the shortcut, it's not possible to reuse the same UDP socket for outgoing connections.
### Running a Client
As mentioned above, multiple outgoing connections can share a single UDP socket, since QUIC uses Connection IDs to demultiplex connections.
```go
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s handshake timeout
defer cancel()
conn, err := tr.Dial(ctx, <server address>, <tls.Config>, <quic.Config>)
// ... error handling
```
As a shortcut, `quic.Dial` and `quic.DialAddr` can be used without explictly initializing a `quic.Transport`:
```go
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s handshake timeout
defer cancel()
conn, err := quic.Dial(ctx, conn, <server address>, <tls.Config>, <quic.Config>)
```
Just as we saw before when used a similar shortcut to run a server, it's also not possible to reuse the same UDP socket for other outgoing connections, or to listen for incoming connections.
### Using a QUIC Connection
#### Accepting Streams
QUIC is a stream-multiplexed transport. A `quic.Connection` fundamentally differs from the `net.Conn` and the `net.PacketConn` interface defined in the standard library. Data is sent and received on (unidirectional and bidirectional) streams (and, if supported, in [datagrams](#quic-datagrams)), not on the connection itself. The stream state machine is described in detail in [Section 3 of RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000#section-3).
Note: A unidirectional stream is a stream that the initiator can only write to (`quic.SendStream`), and the receiver can only read from (`quic.ReceiveStream`). A bidirectional stream (`quic.Stream`) allows reading from and writing to for both sides.
On the receiver side, streams are accepted using the `AcceptStream` (for bidirectional) and `AcceptUniStream` functions. For most user cases, it makes sense to call these functions in a loop:
```go
for {
str, err := conn.AcceptStream(context.Background()) // for bidirectional streams
// ... error handling
// handle the stream, usually in a new Go routine
}
```
These functions return an error when the underlying QUIC connection is closed.
#### Opening Streams
There are two slightly different ways to open streams, one synchronous and one (potentially) asynchronous. This API is necessary since the receiver grants us a certain number of streams that we're allowed to open. It may grant us additional streams later on (typically when existing streams are closed), but it means that at the time we want to open a new stream, we might not be able to do so.
Using the synchronous method `OpenStreamSync` for bidirectional streams, and `OpenUniStreamSync` for unidirectional streams, an application can block until the peer allows opening additional streams. In case that we're allowed to open a new stream, these methods return right away:
```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
str, err := conn.OpenStreamSync(ctx) // wait up to 5s to open a new bidirectional stream
```
The asynchronous version never blocks. If it's currently not possible to open a new stream, it returns a `net.Error` timeout error:
```go
str, err := conn.OpenStream()
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
// It's currently not possible to open another stream,
// but it might be possible later, once the peer allowed us to do so.
}
```
These functions return an error when the underlying QUIC connection is closed.
#### Using Streams
Using QUIC streams is pretty straightforward. The `quic.ReceiveStream` implements the `io.Reader` interface, and the `quic.SendStream` implements the `io.Writer` interface. A bidirectional stream (`quic.Stream`) implements both these interfaces. Conceptually, a bidirectional stream can be thought of as the composition of two unidirectional streams in opposite directions.
Calling `Close` on a `quic.SendStream` or a `quic.Stream` closes the send side of the stream. On the receiver side, this will be surfaced as an `io.EOF` returned from the `io.Reader` once all data has been consumed. Note that for bidirectional streams, `Close` _only_ closes the send side of the stream. It is still possible to read from the stream until the peer closes or resets the stream.
In case the application wishes to abort sending on a `quic.SendStream` or a `quic.Stream` , it can reset the send side by calling `CancelWrite` with an application-defined error code (an unsigned 62-bit number). On the receiver side, this surfaced as a `quic.StreamError` containing that error code on the `io.Reader`. Note that for bidirectional streams, `CancelWrite` _only_ resets the send side of the stream. It is still possible to read from the stream until the peer closes or resets the stream.
Conversely, in case the application wishes to abort receiving from a `quic.ReceiveStream` or a `quic.Stream`, it can ask the sender to abort data transmission by calling `CancelRead` with an application-defined error code (an unsigned 62-bit number). On the receiver side, this surfaced as a `quic.StreamError` containing that error code on the `io.Writer`. Note that for bidirectional streams, `CancelWrite` _only_ resets the receive side of the stream. It is still possible to write to the stream.
A bidirectional stream is only closed once both the read and the write side of the stream have been either closed or reset. Only then the peer is granted a new stream according to the maximum number of concurrent streams configured via `quic.Config.MaxIncomingStreams`.
### Configuring QUIC
The `quic.Config` struct passed to both the listen and dial calls (see above) contains a wide range of configuration options for QUIC connections, incl. the ability to fine-tune flow control limits, the number of streams that the peer is allowed to open concurrently, keep-alives, idle timeouts, and many more. Please refer to the documentation for the `quic.Config` for details.
The `quic.Transport` contains a few configuration options that don't apply to any single QUIC connection, but to all connections handled by that transport. It is highly recommend to set the `StatelessResetToken`, which allows endpoints to quickly recover from crashes / reboots of our node (see [Section 10.3 of RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000#section-10.3)).
### Closing a Connection
#### When the remote Peer closes the Connection
In case the peer closes the QUIC connection, all calls to open streams, accept streams, as well as all methods on streams immediately return an error. Additionally, it is set as cancellation cause of the connection context. Users can use errors assertions to find out what exactly went wrong:
* `quic.VersionNegotiationError`: Happens during the handshake, if there is no overlap between our and the remote's supported QUIC versions.
* `quic.HandshakeTimeoutError`: Happens if the QUIC handshake doesn't complete within the time specified in `quic.Config.HandshakeTimeout`.
* `quic.IdleTimeoutError`: Happens after completion of the handshake if the connection is idle for longer than the minimum of both peers idle timeouts (as configured by `quic.Config.IdleTimeout`). The connection is considered idle when no stream data (and datagrams, if applicable) are exchanged for that period. The QUIC connection can be instructed to regularly send a packet to prevent a connection from going idle by setting `quic.Config.KeepAlive`. However, this is no guarantee that the peer doesn't suddenly go away (e.g. by abruptly shutting down the node or by crashing), or by a NAT binding expiring, in which case this error might still occur.
* `quic.StatelessResetError`: Happens when the remote peer lost the state required to decrypt the packet. This requires the `quic.Transport.StatelessResetToken` to be configured by the peer.
* `quic.TransportError`: Happens if when the QUIC protocol is violated. Unless the error code is `APPLICATION_ERROR`, this will not happen unless one of the QUIC stacks involved is misbehaving. Please open an issue if you encounter this error.
* `quic.ApplicationError`: Happens when the remote decides to close the connection, see below.
#### Initiated by the Application
A `quic.Connection` can be closed using `CloseWithError`:
```go
conn.CloseWithError(0x42, "error 0x42 occurred")
```
Applications can transmit both an error code (an unsigned 62-bit number) as well as a UTF-8 encoded human-readable reason. The error code allows the receiver to learn why the connection was closed, and the reason can be useful for debugging purposes.
On the receiver side, this is surfaced as a `quic.ApplicationError`.
### QUIC Datagrams
Unreliable datagrams are a QUIC extension ([RFC 9221](https://datatracker.ietf.org/doc/html/rfc9221)) that is negotiated during the handshake. Support can be enabled by setting the `quic.Config.EnableDatagram` flag. Note that this doesn't guarantee that the peer also supports datagrams. Whether or not the feature negotiation succeeded can be learned from the `quic.ConnectionState.SupportsDatagrams` obtained from `quic.Connection.ConnectionState()`.
QUIC DATAGRAMs are a new QUIC frame type sent in QUIC 1-RTT packets (i.e. after completion of the handshake). Therefore, they're end-to-end encrypted and congestion-controlled. However, if a DATAGRAM frame is deemed lost by QUIC's loss detection mechanism, they are not retransmitted.
Datagrams are sent using the `SendDatagram` method on the `quic.Connection`:
```go
conn.SendDatagram([]byte("foobar"))
```
And received using `ReceiveDatagram`:
```go
msg, err := conn.ReceiveDatagram()
```
Note that this code path is currently not optimized. It works for datagrams that are sent occasionally, but it doesn't achieve the same throughput as writing data on a stream. Please get in touch on issue #3766 if your use case relies on high datagram throughput, or if you'd like to help fix this issue. There are also some restrictions regarding the maximum message size (see #3599).
### QUIC Event Logging using qlog
quic-go logs a wide range of events defined in [draft-ietf-quic-qlog-quic-events](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-quic-events/), providing comprehensive insights in the internals of a QUIC connection.
qlog files can be processed by a number of 3rd-party tools. [qviz](https://qvis.quictools.info/) has proven very useful for debugging all kinds of QUIC connection failures.
qlog can be activated by setting the `Tracer` callback on the `Config`. It is called as soon as quic-go decides to start the QUIC handshake on a new connection.
`qlog.DefaultTracer` provides a tracer implementation which writes qlog files to a directory specified by the `QLOGDIR` environment variable, if set.
The default qlog tracer can be used like this:
```go
quic.Config{
Tracer: qlog.DefaultTracer,
}
```
This example creates a new qlog file under `<QLOGDIR>/<Original Destination Connection ID>_<Vantage Point>.qlog`, e.g. `qlogs/2e0407da_client.qlog`.
For custom qlog behavior, `qlog.NewConnectionTracer` can be used.
## Using HTTP/3
### As a server
See the [example server](example/main.go). Starting a QUIC server is very similar to the standard library http package in Go:
```go
http.Handle("/", http.FileServer(http.Dir(wwwDir)))
http3.ListenAndServeQUIC("localhost:4242", "/path/to/cert/chain.pem", "/path/to/privkey.pem", nil)
```
### As a client
See the [example client](example/client/main.go). Use a `http3.RoundTripper` as a `Transport` in a `http.Client`.
```go
http.Client{
Transport: &http3.RoundTripper{},
}
```
Detailed documentation can be found on [quic-go.net](https://quic-go.net/docs/).
## Projects using quic-go

View File

@ -35,7 +35,7 @@ type client struct {
conn quicConn
tracer *logging.ConnectionTracer
tracingID uint64
tracingID ConnectionTracingID
logger utils.Logger
}

View File

@ -113,8 +113,8 @@ func (e *errCloseForRecreating) Error() string {
return "closing connection in order to recreate it"
}
var connTracingID uint64 // to be accessed atomically
func nextConnTracingID() uint64 { return atomic.AddUint64(&connTracingID, 1) }
var connTracingID atomic.Uint64 // to be accessed atomically
func nextConnTracingID() ConnectionTracingID { return ConnectionTracingID(connTracingID.Add(1)) }
// A Connection is a QUIC connection
type connection struct {
@ -234,7 +234,7 @@ type connection struct {
tokenGenerator *handshake.TokenGenerator,
clientAddressValidated bool,
tracer *logging.ConnectionTracer,
tracingID uint64,
tracingID ConnectionTracingID,
logger utils.Logger,
v protocol.Version,
) quicConn {
@ -272,8 +272,8 @@ func(connID protocol.ConnectionID) { runner.Add(connID, s) },
s.queueControlFrame,
connIDGenerator,
)
s.preSetup()
s.ctx, s.ctxCancel = context.WithCancelCause(context.WithValue(context.Background(), ConnectionTracingKey, tracingID))
s.preSetup()
s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
0,
getMaxPacketSize(s.conn.RemoteAddr()),
@ -347,7 +347,7 @@ func(connID protocol.ConnectionID) { runner.Add(connID, s) },
enable0RTT bool,
hasNegotiatedVersion bool,
tracer *logging.ConnectionTracer,
tracingID uint64,
tracingID ConnectionTracingID,
logger utils.Logger,
v protocol.Version,
) quicConn {
@ -381,8 +381,8 @@ func(connID protocol.ConnectionID) { runner.Add(connID, s) },
s.queueControlFrame,
connIDGenerator,
)
s.preSetup()
s.ctx, s.ctxCancel = context.WithCancelCause(context.WithValue(context.Background(), ConnectionTracingKey, tracingID))
s.preSetup()
s.sentPacketHandler, s.receivedPacketHandler = ackhandler.NewAckHandler(
initialPacketNumber,
getMaxPacketSize(s.conn.RemoteAddr()),
@ -471,6 +471,7 @@ func(size protocol.ByteCount) bool {
)
s.earlyConnReadyChan = make(chan struct{})
s.streamsMap = newStreamsMap(
s.ctx,
s,
s.newFlowController,
uint64(s.config.MaxIncomingStreams),
@ -2357,10 +2358,9 @@ func (s *connection) SendDatagram(p []byte) error {
}
f := &wire.DatagramFrame{DataLenPresent: true}
if protocol.ByteCount(len(p)) > f.MaxDataLen(s.peerParams.MaxDatagramFrameSize, s.version) {
return &DatagramTooLargeError{
PeerMaxDatagramFrameSize: int64(s.peerParams.MaxDatagramFrameSize),
}
maxDataLen := f.MaxDataLen(s.peerParams.MaxDatagramFrameSize, s.version)
if protocol.ByteCount(len(p)) > maxDataLen {
return &DatagramTooLargeError{MaxDatagramPayloadSize: int64(maxDataLen)}
}
f.Data = make([]byte, len(p))
copy(f.Data, p)

View File

@ -64,7 +64,7 @@ func (e *StreamError) Error() string {
// DatagramTooLargeError is returned from Connection.SendDatagram if the payload is too large to be sent.
type DatagramTooLargeError struct {
PeerMaxDatagramFrameSize int64
MaxDatagramPayloadSize int64
}
func (e *DatagramTooLargeError) Is(target error) bool {

View File

@ -157,7 +157,7 @@ func (f *framerI) AppendStreamFrames(frames []ackhandler.StreamFrame, maxLen pro
// For the last STREAM frame, we'll remove the DataLen field later.
// Therefore, we can pretend to have more bytes available when popping
// the STREAM frame (which will always have the DataLen set).
remainingLen += quicvarint.Len(uint64(remainingLen))
remainingLen += protocol.ByteCount(quicvarint.Len(uint64(remainingLen)))
frame, ok, hasMoreData := str.popStreamFrame(remainingLen, v)
if hasMoreData { // put the stream back in the queue (at the end)
f.streamQueue.PushBack(id)

View File

@ -1,104 +1,9 @@
# HTTP/3
[![Documentation](https://img.shields.io/badge/docs-quic--go.net-red?style=flat)](https://quic-go.net/docs/)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/quic-go/quic-go/http3)](https://pkg.go.dev/github.com/quic-go/quic-go/http3)
This package implements HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)).
This package implements HTTP/3 ([RFC 9114](https://datatracker.ietf.org/doc/html/rfc9114)), including QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)) and HTTP Datagrams ([RFC 9297](https://datatracker.ietf.org/doc/html/rfc9297)).
It aims to provide feature parity with the standard library's HTTP/1.1 and HTTP/2 implementation.
## Serving HTTP/3
The easiest way to start an HTTP/3 server is using
```go
mux := http.NewServeMux()
// ... add HTTP handlers to mux ...
// If mux is nil, the http.DefaultServeMux is used.
http3.ListenAndServeQUIC("0.0.0.0:443", "/path/to/cert", "/path/to/key", mux)
```
`ListenAndServeQUIC` is a convenience function. For more configurability, set up an `http3.Server` explicitly:
```go
server := http3.Server{
Handler: mux,
Addr: "0.0.0.0:443",
TLSConfig: http3.ConfigureTLSConfig(&tls.Config{}), // use your tls.Config here
QuicConfig: &quic.Config{},
}
err := server.ListenAndServe()
```
The `http3.Server` provides a number of configuration options, please refer to the [documentation](https://pkg.go.dev/github.com/quic-go/quic-go/http3#Server) for a complete list. The `QuicConfig` is used to configure the underlying QUIC connection. More details can be found in the documentation of the QUIC package.
It is also possible to manually set up a `quic.Transport`, and then pass the listener to the server. This is useful when you want to set configuration options on the `quic.Transport`.
```go
tr := quic.Transport{Conn: conn}
tlsConf := http3.ConfigureTLSConfig(&tls.Config{}) // use your tls.Config here
quicConf := &quic.Config{} // QUIC connection options
server := http3.Server{}
ln, _ := tr.ListenEarly(tlsConf, quicConf)
server.ServeListener(ln)
```
Alternatively, it is also possible to pass fully established QUIC connections to the HTTP/3 server. This is useful if the QUIC server offers multiple ALPNs (via `NextProtos` in the `tls.Config`).
```go
tr := quic.Transport{Conn: conn}
tlsConf := http3.ConfigureTLSConfig(&tls.Config{}) // use your tls.Config here
quicConf := &quic.Config{} // QUIC connection options
server := http3.Server{}
// alternatively, use tr.ListenEarly to accept 0-RTT connections
ln, _ := tr.Listen(tlsConf, quicConf)
for {
c, _ := ln.Accept()
switch c.ConnectionState().TLS.NegotiatedProtocol {
case http3.NextProtoH3:
go server.ServeQUICConn(c)
// ... handle other protocols ...
}
}
```
## Dialing HTTP/3
This package provides a `http.RoundTripper` implementation that can be used on the `http.Client`:
```go
&http3.RoundTripper{
TLSClientConfig: &tls.Config{}, // set a TLS client config, if desired
QuicConfig: &quic.Config{}, // QUIC connection options
}
defer roundTripper.Close()
client := &http.Client{
Transport: roundTripper,
}
```
The `http3.RoundTripper` provides a number of configuration options, please refer to the [documentation](https://pkg.go.dev/github.com/quic-go/quic-go/http3#RoundTripper) for a complete list.
To use a custom `quic.Transport`, the function used to dial new QUIC connections can be configured:
```go
tr := quic.Transport{}
roundTripper := &http3.RoundTripper{
TLSClientConfig: &tls.Config{}, // set a TLS client config, if desired
QuicConfig: &quic.Config{}, // QUIC connection options
Dial: func(ctx context.Context, addr string, tlsConf *tls.Config, quicConf *quic.Config) (quic.EarlyConnection, error) {
a, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
return tr.DialEarly(ctx, a, tlsConf, quicConf)
},
}
```
## Using the same UDP Socket for Server and Roundtripper
Since QUIC demultiplexes packets based on their connection IDs, it is possible allows running a QUIC server and client on the same UDP socket. This also works when using HTTP/3: HTTP requests can be sent from the same socket that a server is listening on.
To achieve this using this package, first initialize a single `quic.Transport`, and pass a `quic.EarlyListner` obtained from that transport to `http3.Server.ServeListener`, and use the `DialEarly` function of the transport as the `Dial` function for the `http3.RoundTripper`.
## QPACK
HTTP/3 utilizes QPACK ([RFC 9204](https://datatracker.ietf.org/doc/html/rfc9204)) for efficient HTTP header field compression. Our implementation, available at[quic-go/qpack](https://github.com/quic-go/qpack), provides a minimal implementation of the protocol.
While the current implementation is a fully interoperable implementation of the QPACK protocol, it only uses the static compression table. The dynamic table would allow for more effective compression of frequently transmitted header fields. This can be particularly beneficial in scenarios where headers have considerable redundancy or in high-throughput environments.
If you think that your application would benefit from higher compression efficiency, or if you're interested in contributing improvements here, please let us know in [#2424](https://github.com/quic-go/quic-go/issues/2424).
Detailed documentation can be found on [quic-go.net](https://quic-go.net/docs/).

View File

@ -2,68 +2,67 @@
import (
"context"
"errors"
"io"
"net"
"github.com/quic-go/quic-go"
)
// The HTTPStreamer allows taking over a HTTP/3 stream. The interface is implemented by:
// * for the server: the http.Request.Body
// * for the client: the http.Response.Body
// On the client side, the stream will be closed for writing, unless the DontCloseRequestStream RoundTripOpt was set.
// When a stream is taken over, it's the caller's responsibility to close the stream.
type HTTPStreamer interface {
HTTPStream() Stream
}
type StreamCreator interface {
// Context returns a context that is cancelled when the underlying connection is closed.
Context() context.Context
OpenStream() (quic.Stream, error)
OpenStreamSync(context.Context) (quic.Stream, error)
OpenUniStream() (quic.SendStream, error)
OpenUniStreamSync(context.Context) (quic.SendStream, error)
LocalAddr() net.Addr
RemoteAddr() net.Addr
ConnectionState() quic.ConnectionState
}
var _ StreamCreator = quic.Connection(nil)
// A Hijacker allows hijacking of the stream creating part of a quic.Session from a http.Response.Body.
// It is used by WebTransport to create WebTransport streams after a session has been established.
type Hijacker interface {
StreamCreator() StreamCreator
Connection() Connection
}
// The body of a http.Request or http.Response.
var errTooMuchData = errors.New("peer sent too much data")
// The body is used in the requestBody (for a http.Request) and the responseBody (for a http.Response).
type body struct {
str quic.Stream
str *stream
wasHijacked bool // set when HTTPStream is called
remainingContentLength int64
violatedContentLength bool
hasContentLength bool
}
var (
_ io.ReadCloser = &body{}
_ HTTPStreamer = &body{}
)
func newRequestBody(str Stream) *body {
return &body{str: str}
func newBody(str *stream, contentLength int64) *body {
b := &body{str: str}
if contentLength >= 0 {
b.hasContentLength = true
b.remainingContentLength = contentLength
}
return b
}
func (r *body) HTTPStream() Stream {
r.wasHijacked = true
return r.str
}
func (r *body) StreamID() quic.StreamID { return r.str.StreamID() }
func (r *body) wasStreamHijacked() bool {
return r.wasHijacked
func (r *body) checkContentLengthViolation() error {
if !r.hasContentLength {
return nil
}
if r.remainingContentLength < 0 || r.remainingContentLength == 0 && r.str.hasMoreData() {
if !r.violatedContentLength {
r.str.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
r.str.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
r.violatedContentLength = true
}
return errTooMuchData
}
return nil
}
func (r *body) Read(b []byte) (int, error) {
if err := r.checkContentLengthViolation(); err != nil {
return 0, err
}
if r.hasContentLength {
b = b[:min(int64(len(b)), r.remainingContentLength)]
}
n, err := r.str.Read(b)
r.remainingContentLength -= int64(n)
if err := r.checkContentLengthViolation(); err != nil {
return n, err
}
return n, maybeReplaceError(err)
}
@ -72,9 +71,26 @@ func (r *body) Close() error {
return nil
}
type hijackableBody struct {
type requestBody struct {
body
conn quic.Connection // only needed to implement Hijacker
connCtx context.Context
rcvdSettings <-chan struct{}
getSettings func() *Settings
}
var _ io.ReadCloser = &requestBody{}
func newRequestBody(str *stream, contentLength int64, connCtx context.Context, rcvdSettings <-chan struct{}, getSettings func() *Settings) *requestBody {
return &requestBody{
body: *newBody(str, contentLength),
connCtx: connCtx,
rcvdSettings: rcvdSettings,
getSettings: getSettings,
}
}
type hijackableBody struct {
body body
// only set for the http.Response
// The channel is closed when the user is done with this response:
@ -83,27 +99,17 @@ type hijackableBody struct {
reqDoneClosed bool
}
var (
_ Hijacker = &hijackableBody{}
_ HTTPStreamer = &hijackableBody{}
)
var _ io.ReadCloser = &hijackableBody{}
func newResponseBody(str Stream, conn quic.Connection, done chan<- struct{}) *hijackableBody {
func newResponseBody(str *stream, contentLength int64, done chan<- struct{}) *hijackableBody {
return &hijackableBody{
body: body{
str: str,
},
body: *newBody(str, contentLength),
reqDone: done,
conn: conn,
}
}
func (r *hijackableBody) StreamCreator() StreamCreator {
return r.conn
}
func (r *hijackableBody) Read(b []byte) (int, error) {
n, err := r.str.Read(b)
n, err := r.body.Read(b)
if err != nil {
r.requestDone()
}
@ -120,17 +126,9 @@ func (r *hijackableBody) requestDone() {
r.reqDoneClosed = true
}
func (r *body) StreamID() quic.StreamID {
return r.str.StreamID()
}
func (r *hijackableBody) Close() error {
r.requestDone()
// If the EOF was read, CancelRead() is a no-op.
r.str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
r.body.str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
return nil
}
func (r *hijackableBody) HTTPStream() Stream {
return r.str
}

View File

@ -2,28 +2,31 @@
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"log/slog"
"net/http"
"strconv"
"net/http/httptrace"
"net/textproto"
"sync"
"sync/atomic"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/quicvarint"
"github.com/quic-go/qpack"
)
// MethodGet0RTT allows a GET request to be sent using 0-RTT.
// Note that 0-RTT data doesn't provide replay protection.
const MethodGet0RTT = "GET_0RTT"
const (
// MethodGet0RTT allows a GET request to be sent using 0-RTT.
// Note that 0-RTT doesn't provide replay protection and should only be used for idempotent requests.
MethodGet0RTT = "GET_0RTT"
// MethodHead0RTT allows a HEAD request to be sent using 0-RTT.
// Note that 0-RTT doesn't provide replay protection and should only be used for idempotent requests.
MethodHead0RTT = "HEAD_0RTT"
)
const (
defaultUserAgent = "quic-go HTTP/3"
@ -35,122 +38,67 @@
KeepAlivePeriod: 10 * time.Second,
}
type dialFunc func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error)
// SingleDestinationRoundTripper is an HTTP/3 client doing requests to a single remote server.
type SingleDestinationRoundTripper struct {
Connection quic.Connection
var dialAddr dialFunc = quic.DialAddrEarly
// Enable support for HTTP/3 datagrams (RFC 9297).
// If a QUICConfig is set, datagram support also needs to be enabled on the QUIC layer by setting EnableDatagrams.
EnableDatagrams bool
type roundTripperOpts struct {
DisableCompression bool
EnableDatagram bool
MaxHeaderBytes int64
// Additional HTTP/3 settings.
// It is invalid to specify any settings defined by RFC 9114 (HTTP/3) and RFC 9297 (HTTP Datagrams).
AdditionalSettings map[uint64]uint64
StreamHijacker func(FrameType, quic.Connection, quic.Stream, error) (hijacked bool, err error)
UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream, error) (hijacked bool)
}
StreamHijacker func(FrameType, quic.ConnectionTracingID, quic.Stream, error) (hijacked bool, err error)
UniStreamHijacker func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)
// client is a HTTP3 client doing requests
type client struct {
tlsConf *tls.Config
config *quic.Config
opts *roundTripperOpts
// MaxResponseHeaderBytes specifies a limit on how many response bytes are
// allowed in the server's response header.
// Zero means to use a default limit.
MaxResponseHeaderBytes int64
dialOnce sync.Once
dialer dialFunc
handshakeErr error
// DisableCompression, if true, prevents the Transport from requesting compression with an
// "Accept-Encoding: gzip" request header when the Request contains no existing Accept-Encoding value.
// If the Transport requests gzip on its own and gets a gzipped response, it's transparently
// decoded in the Response.Body.
// However, if the user explicitly requested gzip it is not automatically uncompressed.
DisableCompression bool
receivedSettings chan struct{} // closed once the server's SETTINGS frame was processed
settings *Settings // set once receivedSettings is closed
Logger *slog.Logger
initOnce sync.Once
hconn *connection
requestWriter *requestWriter
decoder *qpack.Decoder
hostname string
conn atomic.Pointer[quic.EarlyConnection]
logger utils.Logger
decoder *qpack.Decoder
}
var _ roundTripCloser = &client{}
var _ http.RoundTripper = &SingleDestinationRoundTripper{}
func newClient(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) {
if conf == nil {
conf = defaultQuicConfig.Clone()
conf.EnableDatagrams = opts.EnableDatagram
}
if opts.EnableDatagram && !conf.EnableDatagrams {
return nil, errors.New("HTTP Datagrams enabled, but QUIC Datagrams disabled")
}
if len(conf.Versions) == 0 {
conf = conf.Clone()
conf.Versions = []quic.Version{protocol.SupportedVersions[0]}
}
if len(conf.Versions) != 1 {
return nil, errors.New("can only use a single QUIC version for dialing a HTTP/3 connection")
}
if conf.MaxIncomingStreams == 0 {
conf.MaxIncomingStreams = -1 // don't allow any bidirectional streams
}
logger := utils.DefaultLogger.WithPrefix("h3 client")
if tlsConf == nil {
tlsConf = &tls.Config{}
} else {
tlsConf = tlsConf.Clone()
}
if tlsConf.ServerName == "" {
sni, _, err := net.SplitHostPort(hostname)
if err != nil {
// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
sni = hostname
}
tlsConf.ServerName = sni
}
// Replace existing ALPNs by H3
tlsConf.NextProtos = []string{versionToALPN(conf.Versions[0])}
return &client{
hostname: authorityAddr("https", hostname),
tlsConf: tlsConf,
requestWriter: newRequestWriter(logger),
receivedSettings: make(chan struct{}),
decoder: qpack.NewDecoder(func(hf qpack.HeaderField) {}),
config: conf,
opts: opts,
dialer: dialer,
logger: logger,
}, nil
func (c *SingleDestinationRoundTripper) Start() Connection {
c.initOnce.Do(func() { c.init() })
return c.hconn
}
func (c *client) dial(ctx context.Context) error {
var err error
var conn quic.EarlyConnection
if c.dialer != nil {
conn, err = c.dialer(ctx, c.hostname, c.tlsConf, c.config)
} else {
conn, err = dialAddr(ctx, c.hostname, c.tlsConf, c.config)
}
if err != nil {
return err
}
c.conn.Store(&conn)
func (c *SingleDestinationRoundTripper) init() {
c.decoder = qpack.NewDecoder(func(hf qpack.HeaderField) {})
c.requestWriter = newRequestWriter()
c.hconn = newConnection(c.Connection, c.EnableDatagrams, protocol.PerspectiveClient, c.Logger)
// send the SETTINGs frame, using 0-RTT data, if possible
go func() {
if err := c.setupConn(conn); err != nil {
c.logger.Debugf("Setting up connection failed: %s", err)
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
if err := c.setupConn(c.hconn); err != nil {
if c.Logger != nil {
c.Logger.Debug("Setting up connection failed", "error", err)
}
c.hconn.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
}
}()
if c.opts.StreamHijacker != nil {
go c.handleBidirectionalStreams(conn)
if c.StreamHijacker != nil {
go c.handleBidirectionalStreams()
}
go c.handleUnidirectionalStreams(conn)
return nil
go c.hconn.HandleUnidirectionalStreams(c.UniStreamHijacker)
}
func (c *client) setupConn(conn quic.EarlyConnection) error {
func (c *SingleDestinationRoundTripper) setupConn(conn *connection) error {
// open the control stream
str, err := conn.OpenUniStream()
if err != nil {
@ -159,122 +107,50 @@ func (c *client) setupConn(conn quic.EarlyConnection) error {
b := make([]byte, 0, 64)
b = quicvarint.Append(b, streamTypeControlStream)
// send the SETTINGS frame
b = (&settingsFrame{Datagram: c.opts.EnableDatagram, Other: c.opts.AdditionalSettings}).Append(b)
b = (&settingsFrame{Datagram: c.EnableDatagrams, Other: c.AdditionalSettings}).Append(b)
_, err = str.Write(b)
return err
}
func (c *client) handleBidirectionalStreams(conn quic.EarlyConnection) {
func (c *SingleDestinationRoundTripper) handleBidirectionalStreams() {
for {
str, err := conn.AcceptStream(context.Background())
str, err := c.hconn.AcceptStream(context.Background())
if err != nil {
c.logger.Debugf("accepting bidirectional stream failed: %s", err)
if c.Logger != nil {
c.Logger.Debug("accepting bidirectional stream failed", "error", err)
}
return
}
go func(str quic.Stream) {
_, err := parseNextFrame(str, func(ft FrameType, e error) (processed bool, err error) {
return c.opts.StreamHijacker(ft, conn, str, e)
id := c.hconn.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
return c.StreamHijacker(ft, id, str, e)
})
if err == errHijacked {
return
}
if err != nil {
c.logger.Debugf("error handling stream: %s", err)
if c.Logger != nil {
c.Logger.Debug("error handling stream", "error", err)
}
}
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
c.hconn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
}(str)
}
}
func (c *client) handleUnidirectionalStreams(conn quic.EarlyConnection) {
var rcvdControlStream atomic.Bool
for {
str, err := conn.AcceptUniStream(context.Background())
if err != nil {
c.logger.Debugf("accepting unidirectional stream failed: %s", err)
return
}
go func(str quic.ReceiveStream) {
streamType, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
if c.opts.UniStreamHijacker != nil && c.opts.UniStreamHijacker(StreamType(streamType), conn, str, err) {
return
}
c.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err)
return
}
// We're only interested in the control stream here.
switch streamType {
case streamTypeControlStream:
case streamTypeQPACKEncoderStream, streamTypeQPACKDecoderStream:
// Our QPACK implementation doesn't use the dynamic table yet.
// TODO: check that only one stream of each type is opened.
return
case streamTypePushStream:
// We never increased the Push ID, so we don't expect any push streams.
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
return
default:
if c.opts.UniStreamHijacker != nil && c.opts.UniStreamHijacker(StreamType(streamType), conn, str, nil) {
return
}
str.CancelRead(quic.StreamErrorCode(ErrCodeStreamCreationError))
return
}
// Only a single control stream is allowed.
if isFirstControlStr := rcvdControlStream.CompareAndSwap(false, true); !isFirstControlStr {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate control stream")
return
}
f, err := parseNextFrame(str, nil)
if err != nil {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
return
}
sf, ok := f.(*settingsFrame)
if !ok {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
return
}
c.settings = &Settings{
EnableDatagram: sf.Datagram,
EnableExtendedConnect: sf.ExtendedConnect,
Other: sf.Other,
}
close(c.receivedSettings)
if !sf.Datagram {
return
}
// If datagram support was enabled on our side as well as on the server side,
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
if c.opts.EnableDatagram && !conn.ConnectionState().SupportsDatagrams {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
}
}(str)
}
}
func (c *client) Close() error {
conn := c.conn.Load()
if conn == nil {
return nil
}
return (*conn).CloseWithError(quic.ApplicationErrorCode(ErrCodeNoError), "")
}
func (c *client) maxHeaderBytes() uint64 {
if c.opts.MaxHeaderBytes <= 0 {
func (c *SingleDestinationRoundTripper) maxHeaderBytes() uint64 {
if c.MaxResponseHeaderBytes <= 0 {
return defaultMaxResponseHeaderBytes
}
return uint64(c.opts.MaxHeaderBytes)
return uint64(c.MaxResponseHeaderBytes)
}
// RoundTripOpt executes a request and returns a response
func (c *client) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
rsp, err := c.roundTripOpt(req, opt)
// RoundTrip executes a request and returns a response
func (c *SingleDestinationRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
c.initOnce.Do(func() { c.init() })
rsp, err := c.roundTrip(req)
if err != nil && req.Context().Err() != nil {
// if the context was canceled, return the context cancellation error
err = req.Context().Err()
@ -282,46 +158,48 @@ func (c *client) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Respon
return rsp, err
}
func (c *client) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
if authorityAddr("https", hostnameFromRequest(req)) != c.hostname {
return nil, fmt.Errorf("http3 client BUG: RoundTripOpt called for the wrong client (expected %s, got %s)", c.hostname, req.Host)
}
c.dialOnce.Do(func() {
c.handshakeErr = c.dial(req.Context())
})
if c.handshakeErr != nil {
return nil, c.handshakeErr
}
// At this point, c.conn is guaranteed to be set.
conn := *c.conn.Load()
func (c *SingleDestinationRoundTripper) roundTrip(req *http.Request) (*http.Response, error) {
// Immediately send out this request, if this is a 0-RTT request.
if req.Method == MethodGet0RTT {
switch req.Method {
case MethodGet0RTT:
// don't modify the original request
reqCopy := *req
req = &reqCopy
req.Method = http.MethodGet
} else {
case MethodHead0RTT:
// don't modify the original request
reqCopy := *req
req = &reqCopy
req.Method = http.MethodHead
default:
// wait for the handshake to complete
select {
case <-conn.HandshakeComplete():
case <-req.Context().Done():
return nil, req.Context().Err()
earlyConn, ok := c.Connection.(quic.EarlyConnection)
if ok {
select {
case <-earlyConn.HandshakeComplete():
case <-req.Context().Done():
return nil, req.Context().Err()
}
}
}
if opt.CheckSettings != nil {
// It is only possible to send an Extended CONNECT request once the SETTINGS were received.
// See section 3 of RFC 8441.
if isExtendedConnectRequest(req) {
connCtx := c.Connection.Context()
// wait for the server's SETTINGS frame to arrive
select {
case <-c.receivedSettings:
case <-conn.Context().Done():
return nil, context.Cause(conn.Context())
case <-c.hconn.ReceivedSettings():
case <-connCtx.Done():
return nil, context.Cause(connCtx)
}
if err := opt.CheckSettings(*c.settings); err != nil {
return nil, err
if !c.hconn.Settings().EnableExtendedConnect {
return nil, errors.New("http3: server didn't enable Extended CONNECT")
}
}
str, err := conn.OpenStreamSync(req.Context())
reqDone := make(chan struct{})
str, err := c.hconn.openRequestStream(req.Context(), c.requestWriter, reqDone, c.DisableCompression, c.maxHeaderBytes())
if err != nil {
return nil, err
}
@ -329,7 +207,6 @@ func (c *client) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Respon
// Request Cancellation:
// This go routine keeps running even after RoundTripOpt() returns.
// It is shut down when the application is done processing the body.
reqDone := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
@ -341,31 +218,19 @@ func (c *client) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Respon
}
}()
doneChan := reqDone
if opt.DontCloseRequestStream {
doneChan = nil
}
rsp, rerr := c.doRequest(req, conn, str, opt, doneChan)
if rerr.err != nil { // if any error occurred
rsp, err := c.doRequest(req, str)
if err != nil { // if any error occurred
close(reqDone)
<-done
if rerr.streamErr != 0 { // if it was a stream error
str.CancelWrite(quic.StreamErrorCode(rerr.streamErr))
}
if rerr.connErr != 0 { // if it was a connection error
var reason string
if rerr.err != nil {
reason = rerr.err.Error()
}
conn.CloseWithError(quic.ApplicationErrorCode(rerr.connErr), reason)
}
return nil, maybeReplaceError(rerr.err)
return nil, maybeReplaceError(err)
}
if opt.DontCloseRequestStream {
close(reqDone)
<-done
}
return rsp, maybeReplaceError(rerr.err)
return rsp, maybeReplaceError(err)
}
func (c *SingleDestinationRoundTripper) OpenRequestStream(ctx context.Context) (RequestStream, error) {
c.initOnce.Do(func() { c.init() })
return c.hconn.openRequestStream(ctx, c.requestWriter, nil, c.DisableCompression, c.maxHeaderBytes())
}
// cancelingReader reads from the io.Reader.
@ -383,7 +248,7 @@ func (r *cancelingReader) Read(b []byte) (int, error) {
return n, err
}
func (c *client) sendRequestBody(str Stream, body io.ReadCloser, contentLength int64) error {
func (c *SingleDestinationRoundTripper) sendRequestBody(str Stream, body io.ReadCloser, contentLength int64) error {
defer body.Close()
buf := make([]byte, bodyCopyBufferSize)
sr := &cancelingReader{str: str, r: body}
@ -407,21 +272,13 @@ func (c *client) sendRequestBody(str Stream, body io.ReadCloser, contentLength i
return err
}
func (c *client) doRequest(req *http.Request, conn quic.EarlyConnection, str quic.Stream, opt RoundTripOpt, reqDone chan<- struct{}) (*http.Response, requestError) {
var requestGzip bool
if !c.opts.DisableCompression && req.Method != "HEAD" && req.Header.Get("Accept-Encoding") == "" && req.Header.Get("Range") == "" {
requestGzip = true
func (c *SingleDestinationRoundTripper) doRequest(req *http.Request, str *requestStream) (*http.Response, error) {
if err := str.SendRequestHeader(req); err != nil {
return nil, err
}
if err := c.requestWriter.WriteRequestHeader(str, req, requestGzip); err != nil {
return nil, newStreamError(ErrCodeInternalError, err)
}
if req.Body == nil && !opt.DontCloseRequestStream {
if req.Body == nil {
str.Close()
}
hstr := newStream(str, func() { conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "") })
if req.Body != nil {
} else {
// send the request body asynchronously
go func() {
contentLength := int64(-1)
@ -430,89 +287,47 @@ func (c *client) doRequest(req *http.Request, conn quic.EarlyConnection, str qui
if req.ContentLength > 0 {
contentLength = req.ContentLength
}
if err := c.sendRequestBody(hstr, req.Body, contentLength); err != nil {
c.logger.Errorf("Error writing request: %s", err)
}
if !opt.DontCloseRequestStream {
hstr.Close()
if err := c.sendRequestBody(str, req.Body, contentLength); err != nil {
if c.Logger != nil {
c.Logger.Debug("error writing request", "error", err)
}
}
str.Close()
}()
}
frame, err := parseNextFrame(str, nil)
if err != nil {
return nil, newStreamError(ErrCodeFrameError, err)
}
hf, ok := frame.(*headersFrame)
if !ok {
return nil, newConnError(ErrCodeFrameUnexpected, errors.New("expected first frame to be a HEADERS frame"))
}
if hf.Length > c.maxHeaderBytes() {
return nil, newStreamError(ErrCodeFrameError, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", hf.Length, c.maxHeaderBytes()))
}
headerBlock := make([]byte, hf.Length)
if _, err := io.ReadFull(str, headerBlock); err != nil {
return nil, newStreamError(ErrCodeRequestIncomplete, err)
}
hfs, err := c.decoder.DecodeFull(headerBlock)
if err != nil {
// TODO: use the right error code
return nil, newConnError(ErrCodeGeneralProtocolError, err)
}
// copy from net/http: support 1xx responses
trace := httptrace.ContextClientTrace(req.Context())
num1xx := 0 // number of informational 1xx headers received
const max1xxResponses = 5 // arbitrary bound on number of informational responses
res, err := responseFromHeaders(hfs)
if err != nil {
return nil, newStreamError(ErrCodeMessageError, err)
var res *http.Response
for {
var err error
res, err = str.ReadResponse()
if err != nil {
return nil, err
}
resCode := res.StatusCode
is1xx := 100 <= resCode && resCode <= 199
// treat 101 as a terminal status, see https://github.com/golang/go/issues/26161
is1xxNonTerminal := is1xx && resCode != http.StatusSwitchingProtocols
if is1xxNonTerminal {
num1xx++
if num1xx > max1xxResponses {
return nil, errors.New("http: too many 1xx informational responses")
}
if trace != nil && trace.Got1xxResponse != nil {
if err := trace.Got1xxResponse(resCode, textproto.MIMEHeader(res.Header)); err != nil {
return nil, err
}
}
continue
}
break
}
connState := conn.ConnectionState().TLS
connState := c.hconn.ConnectionState().TLS
res.TLS = &connState
res.Request = req
// 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.
var httpStr Stream
if _, ok := res.Header["Content-Length"]; ok && res.ContentLength >= 0 {
httpStr = newLengthLimitedStream(hstr, res.ContentLength)
} else {
httpStr = hstr
}
respBody := newResponseBody(httpStr, conn, 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 := req.Method == http.MethodConnect && 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 requestGzip && res.Header.Get("Content-Encoding") == "gzip" {
res.Header.Del("Content-Encoding")
res.Header.Del("Content-Length")
res.ContentLength = -1
res.Body = newGzipReader(respBody)
res.Uncompressed = true
} else {
res.Body = respBody
}
return res, requestError{}
}
func (c *client) HandshakeComplete() bool {
conn := c.conn.Load()
if conn == nil {
return false
}
select {
case <-(*conn).HandshakeComplete():
return true
default:
return false
}
return res, nil
}

283
vendor/github.com/quic-go/quic-go/http3/conn.go generated vendored Normal file
View File

@ -0,0 +1,283 @@
package http3
import (
"bytes"
"context"
"fmt"
"log/slog"
"net"
"sync"
"sync/atomic"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/quicvarint"
"github.com/quic-go/qpack"
)
// Connection is an HTTP/3 connection.
// It has all methods from the quic.Connection expect for AcceptStream, AcceptUniStream,
// SendDatagram and ReceiveDatagram.
type Connection interface {
OpenStream() (quic.Stream, error)
OpenStreamSync(context.Context) (quic.Stream, error)
OpenUniStream() (quic.SendStream, error)
OpenUniStreamSync(context.Context) (quic.SendStream, error)
LocalAddr() net.Addr
RemoteAddr() net.Addr
CloseWithError(quic.ApplicationErrorCode, string) error
Context() context.Context
ConnectionState() quic.ConnectionState
// ReceivedSettings returns a channel that is closed once the client's SETTINGS frame was received.
ReceivedSettings() <-chan struct{}
// Settings returns the settings received on this connection.
Settings() *Settings
}
type connection struct {
quic.Connection
perspective protocol.Perspective
logger *slog.Logger
enableDatagrams bool
decoder *qpack.Decoder
streamMx sync.Mutex
streams map[protocol.StreamID]*datagrammer
settings *Settings
receivedSettings chan struct{}
}
func newConnection(
quicConn quic.Connection,
enableDatagrams bool,
perspective protocol.Perspective,
logger *slog.Logger,
) *connection {
c := &connection{
Connection: quicConn,
perspective: perspective,
logger: logger,
enableDatagrams: enableDatagrams,
decoder: qpack.NewDecoder(func(hf qpack.HeaderField) {}),
receivedSettings: make(chan struct{}),
streams: make(map[protocol.StreamID]*datagrammer),
}
return c
}
func (c *connection) onStreamStateChange(id quic.StreamID, state streamState, e error) {
c.streamMx.Lock()
defer c.streamMx.Unlock()
d, ok := c.streams[id]
if !ok { // should never happen
return
}
var isDone bool
//nolint:exhaustive // These are all the cases we care about.
switch state {
case streamStateReceiveClosed:
isDone = d.SetReceiveError(e)
case streamStateSendClosed:
isDone = d.SetSendError(e)
default:
return
}
if isDone {
delete(c.streams, id)
}
}
func (c *connection) openRequestStream(
ctx context.Context,
requestWriter *requestWriter,
reqDone chan<- struct{},
disableCompression bool,
maxHeaderBytes uint64,
) (*requestStream, error) {
str, err := c.Connection.OpenStreamSync(ctx)
if err != nil {
return nil, err
}
datagrams := newDatagrammer(func(b []byte) error { return c.sendDatagram(str.StreamID(), b) })
c.streamMx.Lock()
c.streams[str.StreamID()] = datagrams
c.streamMx.Unlock()
qstr := newStateTrackingStream(str, func(s streamState, e error) { c.onStreamStateChange(str.StreamID(), s, e) })
hstr := newStream(qstr, c, datagrams)
return newRequestStream(hstr, requestWriter, reqDone, c.decoder, disableCompression, maxHeaderBytes), nil
}
func (c *connection) acceptStream(ctx context.Context) (quic.Stream, *datagrammer, error) {
str, err := c.AcceptStream(ctx)
if err != nil {
return nil, nil, err
}
datagrams := newDatagrammer(func(b []byte) error { return c.sendDatagram(str.StreamID(), b) })
if c.perspective == protocol.PerspectiveServer {
c.streamMx.Lock()
c.streams[str.StreamID()] = datagrams
c.streamMx.Unlock()
str = newStateTrackingStream(str, func(s streamState, e error) { c.onStreamStateChange(str.StreamID(), s, e) })
}
return str, datagrams, nil
}
func (c *connection) HandleUnidirectionalStreams(hijack func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)) {
var (
rcvdControlStr atomic.Bool
rcvdQPACKEncoderStr atomic.Bool
rcvdQPACKDecoderStr atomic.Bool
)
for {
str, err := c.Connection.AcceptUniStream(context.Background())
if err != nil {
if c.logger != nil {
c.logger.Debug("accepting unidirectional stream failed", "error", err)
}
return
}
go func(str quic.ReceiveStream) {
streamType, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
id := c.Connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
if hijack != nil && hijack(StreamType(streamType), id, str, err) {
return
}
if c.logger != nil {
c.logger.Debug("reading stream type on stream failed", "stream ID", str.StreamID(), "error", err)
}
return
}
// We're only interested in the control stream here.
switch streamType {
case streamTypeControlStream:
case streamTypeQPACKEncoderStream:
if isFirst := rcvdQPACKEncoderStr.CompareAndSwap(false, true); !isFirst {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK encoder stream")
}
// Our QPACK implementation doesn't use the dynamic table yet.
return
case streamTypeQPACKDecoderStream:
if isFirst := rcvdQPACKDecoderStr.CompareAndSwap(false, true); !isFirst {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate QPACK decoder stream")
}
// Our QPACK implementation doesn't use the dynamic table yet.
return
case streamTypePushStream:
switch c.perspective {
case protocol.PerspectiveClient:
// we never increased the Push ID, so we don't expect any push streams
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
case protocol.PerspectiveServer:
// only the server can push
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
}
return
default:
if hijack != nil {
if hijack(
StreamType(streamType),
c.Connection.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID),
str,
nil,
) {
return
}
}
str.CancelRead(quic.StreamErrorCode(ErrCodeStreamCreationError))
return
}
// Only a single control stream is allowed.
if isFirstControlStr := rcvdControlStr.CompareAndSwap(false, true); !isFirstControlStr {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate control stream")
return
}
f, err := parseNextFrame(str, nil)
if err != nil {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
return
}
sf, ok := f.(*settingsFrame)
if !ok {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
return
}
c.settings = &Settings{
EnableDatagrams: sf.Datagram,
EnableExtendedConnect: sf.ExtendedConnect,
Other: sf.Other,
}
close(c.receivedSettings)
if !sf.Datagram {
return
}
// If datagram support was enabled on our side as well as on the server side,
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
if c.enableDatagrams && !c.Connection.ConnectionState().SupportsDatagrams {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
return
}
go func() {
if err := c.receiveDatagrams(); err != nil {
if c.logger != nil {
c.logger.Debug("receiving datagrams failed", "error", err)
}
}
}()
}(str)
}
}
func (c *connection) sendDatagram(streamID protocol.StreamID, b []byte) error {
// TODO: this creates a lot of garbage and an additional copy
data := make([]byte, 0, len(b)+8)
data = quicvarint.Append(data, uint64(streamID/4))
data = append(data, b...)
return c.Connection.SendDatagram(data)
}
func (c *connection) receiveDatagrams() error {
for {
b, err := c.Connection.ReceiveDatagram(context.Background())
if err != nil {
return err
}
// TODO: this is quite wasteful in terms of allocations
r := bytes.NewReader(b)
quarterStreamID, err := quicvarint.Read(r)
if err != nil {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
return fmt.Errorf("could not read quarter stream id: %w", err)
}
if quarterStreamID > maxQuarterStreamID {
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeDatagramError), "")
return fmt.Errorf("invalid quarter stream id: %w", err)
}
streamID := protocol.StreamID(4 * quarterStreamID)
c.streamMx.Lock()
dg, ok := c.streams[streamID]
if !ok {
c.streamMx.Unlock()
return nil
}
c.streamMx.Unlock()
dg.enqueue(b[len(b)-r.Len():])
}
}
// ReceivedSettings returns a channel that is closed once the peer's SETTINGS frame was received.
func (c *connection) ReceivedSettings() <-chan struct{} { return c.receivedSettings }
// Settings returns the settings received on this connection.
// It is only valid to call this function after the channel returned by ReceivedSettings was closed.
func (c *connection) Settings() *Settings { return c.settings }

100
vendor/github.com/quic-go/quic-go/http3/datagram.go generated vendored Normal file
View File

@ -0,0 +1,100 @@
package http3
import (
"context"
"sync"
)
const maxQuarterStreamID = 1<<60 - 1
const streamDatagramQueueLen = 32
type datagrammer struct {
sendDatagram func([]byte) error
hasData chan struct{}
queue [][]byte // TODO: use a ring buffer
mx sync.Mutex
sendErr error
receiveErr error
}
func newDatagrammer(sendDatagram func([]byte) error) *datagrammer {
return &datagrammer{
sendDatagram: sendDatagram,
hasData: make(chan struct{}, 1),
}
}
func (d *datagrammer) SetReceiveError(err error) (isDone bool) {
d.mx.Lock()
defer d.mx.Unlock()
d.receiveErr = err
d.signalHasData()
return d.sendErr != nil
}
func (d *datagrammer) SetSendError(err error) (isDone bool) {
d.mx.Lock()
defer d.mx.Unlock()
d.sendErr = err
return d.receiveErr != nil
}
func (d *datagrammer) Send(b []byte) error {
d.mx.Lock()
sendErr := d.sendErr
d.mx.Unlock()
if sendErr != nil {
return sendErr
}
return d.sendDatagram(b)
}
func (d *datagrammer) signalHasData() {
select {
case d.hasData <- struct{}{}:
default:
}
}
func (d *datagrammer) enqueue(data []byte) {
d.mx.Lock()
defer d.mx.Unlock()
if d.receiveErr != nil {
return
}
if len(d.queue) >= streamDatagramQueueLen {
return
}
d.queue = append(d.queue, data)
d.signalHasData()
}
func (d *datagrammer) Receive(ctx context.Context) ([]byte, error) {
start:
d.mx.Lock()
if len(d.queue) >= 1 {
data := d.queue[0]
d.queue = d.queue[1:]
d.mx.Unlock()
return data, nil
}
if d.receiveErr != nil {
d.mx.Unlock()
return nil, d.receiveErr
}
d.mx.Unlock()
select {
case <-ctx.Done():
return nil, context.Cause(ctx)
case <-d.hasData:
}
goto start
}

View File

@ -6,7 +6,6 @@
"fmt"
"io"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/quicvarint"
)
@ -160,7 +159,7 @@ func parseSettingsFrame(r io.Reader, l uint64) (*settingsFrame, error) {
func (f *settingsFrame) Append(b []byte) []byte {
b = quicvarint.Append(b, 0x4)
var l protocol.ByteCount
var l int
for id, val := range f.Other {
l += quicvarint.Len(id) + quicvarint.Len(val)
}

View File

@ -171,9 +171,9 @@ func requestFromHeaders(headerFields []qpack.HeaderField) (*http.Request, error)
}, nil
}
func hostnameFromRequest(req *http.Request) string {
if req.URL != nil {
return req.URL.Host
func hostnameFromURL(url *url.URL) string {
if url != nil {
return url.Host
}
return ""
}

View File

@ -1,34 +1,64 @@
package http3
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/qpack"
)
// A Stream is a HTTP/3 stream.
// A Stream is an HTTP/3 request stream.
// When writing to and reading from the stream, data is framed in HTTP/3 DATA frames.
type Stream quic.Stream
// The stream conforms to the quic.Stream interface, but instead of writing to and reading directly
// from the QUIC stream, it writes to and reads from the HTTP stream.
type stream struct {
type Stream interface {
quic.Stream
buf []byte
SendDatagram([]byte) error
ReceiveDatagram(context.Context) ([]byte, error)
}
// A RequestStream is an HTTP/3 request stream.
// When writing to and reading from the stream, data is framed in HTTP/3 DATA frames.
type RequestStream interface {
Stream
// SendRequestHeader sends the HTTP request.
// It is invalid to call it more than once.
// It is invalid to call it after Write has been called.
SendRequestHeader(req *http.Request) error
// ReadResponse reads the HTTP response from the stream.
// It is invalid to call it more than once.
// It doesn't set Response.Request and Response.TLS.
// It is invalid to call it after Read has been called.
ReadResponse() (*http.Response, error)
}
type stream struct {
quic.Stream
conn *connection
buf []byte // used as a temporary buffer when writing the HTTP/3 frame headers
onFrameError func()
bytesRemainingInFrame uint64
datagrams *datagrammer
}
var _ Stream = &stream{}
func newStream(str quic.Stream, onFrameError func()) *stream {
func newStream(str quic.Stream, conn *connection, datagrams *datagrammer) *stream {
return &stream{
Stream: str,
onFrameError: onFrameError,
buf: make([]byte, 0, 16),
Stream: str,
conn: conn,
buf: make([]byte, 16),
datagrams: datagrams,
}
}
@ -48,7 +78,7 @@ func (s *stream) Read(b []byte) (int, error) {
s.bytesRemainingInFrame = f.Length
break parseLoop
default:
s.onFrameError()
s.conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "")
// parseNextFrame skips over unknown frame types
// Therefore, this condition is only entered when we parsed another known frame type.
return 0, fmt.Errorf("peer sent an unexpected frame: %T", f)
@ -80,44 +110,150 @@ func (s *stream) Write(b []byte) (int, error) {
return s.Stream.Write(b)
}
var errTooMuchData = errors.New("peer sent too much data")
func (s *stream) writeUnframed(b []byte) (int, error) {
return s.Stream.Write(b)
}
type lengthLimitedStream struct {
func (s *stream) StreamID() protocol.StreamID {
return s.Stream.StreamID()
}
// The stream conforms to the quic.Stream interface, but instead of writing to and reading directly
// from the QUIC stream, it writes to and reads from the HTTP stream.
type requestStream struct {
*stream
contentLength int64
read int64
resetStream bool
responseBody io.ReadCloser // set by ReadResponse
decoder *qpack.Decoder
requestWriter *requestWriter
maxHeaderBytes uint64
reqDone chan<- struct{}
disableCompression bool
sentRequest bool
requestedGzip bool
isConnect bool
}
var _ Stream = &lengthLimitedStream{}
var _ RequestStream = &requestStream{}
func newLengthLimitedStream(str *stream, contentLength int64) *lengthLimitedStream {
return &lengthLimitedStream{
stream: str,
contentLength: contentLength,
func newRequestStream(
str *stream,
requestWriter *requestWriter,
reqDone chan<- struct{},
decoder *qpack.Decoder,
disableCompression bool,
maxHeaderBytes uint64,
) *requestStream {
return &requestStream{
stream: str,
requestWriter: requestWriter,
reqDone: reqDone,
decoder: decoder,
disableCompression: disableCompression,
maxHeaderBytes: maxHeaderBytes,
}
}
func (s *lengthLimitedStream) checkContentLengthViolation() error {
if s.read > s.contentLength || s.read == s.contentLength && s.hasMoreData() {
if !s.resetStream {
s.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
s.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
s.resetStream = true
func (s *requestStream) Read(b []byte) (int, error) {
if s.responseBody == nil {
return 0, errors.New("http3: invalid use of RequestStream.Read: need to call ReadResponse first")
}
return s.responseBody.Read(b)
}
func (s *requestStream) SendRequestHeader(req *http.Request) error {
if s.sentRequest {
return errors.New("http3: invalid duplicate use of SendRequestHeader")
}
if !s.disableCompression && req.Method != http.MethodHead &&
req.Header.Get("Accept-Encoding") == "" && req.Header.Get("Range") == "" {
s.requestedGzip = true
}
s.isConnect = req.Method == http.MethodConnect
s.sentRequest = true
return s.requestWriter.WriteRequestHeader(s.Stream, req, s.requestedGzip)
}
func (s *requestStream) ReadResponse() (*http.Response, error) {
frame, err := parseNextFrame(s.Stream, nil)
if err != nil {
s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
return nil, fmt.Errorf("http3: parsing frame failed: %w", err)
}
hf, ok := frame.(*headersFrame)
if !ok {
s.conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "expected first frame to be a HEADERS frame")
return nil, errors.New("http3: expected first frame to be a HEADERS frame")
}
if hf.Length > s.maxHeaderBytes {
s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
return nil, fmt.Errorf("http3: HEADERS frame too large: %d bytes (max: %d)", hf.Length, s.maxHeaderBytes)
}
headerBlock := make([]byte, hf.Length)
if _, err := io.ReadFull(s.Stream, headerBlock); err != nil {
s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
return nil, fmt.Errorf("http3: failed to read response headers: %w", err)
}
hfs, err := s.decoder.DecodeFull(headerBlock)
if err != nil {
// TODO: use the right error code
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 {
s.Stream.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
s.Stream.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
return nil, fmt.Errorf("http3: invalid response: %w", err)
}
// 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)
// 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
}
}
return errTooMuchData
}
return nil
if s.requestedGzip && res.Header.Get("Content-Encoding") == "gzip" {
res.Header.Del("Content-Encoding")
res.Header.Del("Content-Length")
res.ContentLength = -1
s.responseBody = newGzipReader(respBody)
res.Uncompressed = true
} else {
s.responseBody = respBody
}
res.Body = s.responseBody
return res, nil
}
func (s *lengthLimitedStream) Read(b []byte) (int, error) {
if err := s.checkContentLengthViolation(); err != nil {
return 0, err
}
n, err := s.stream.Read(b[:min(int64(len(b)), s.contentLength-s.read)])
s.read += int64(n)
if err := s.checkContentLengthViolation(); err != nil {
return n, err
}
return n, err
func (s *stream) SendDatagram(b []byte) error {
// TODO: reject if datagrams are not negotiated (yet)
return s.conn.sendDatagram(s.Stream.StreamID(), b)
}
func (s *stream) ReceiveDatagram(ctx context.Context) ([]byte, error) {
// TODO: reject if datagrams are not negotiated (yet)
return s.datagrams.Receive(ctx)
}

View File

@ -2,7 +2,7 @@
package http3
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package http3 -destination mock_roundtripcloser_test.go github.com/quic-go/quic-go/http3 RoundTripCloser"
type RoundTripCloser = roundTripCloser
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package http3 -destination mock_singleroundtripper_test.go github.com/quic-go/quic-go/http3 SingleRoundTripper"
type SingleRoundTripper = singleRoundTripper
//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -package http3 -destination mock_quic_early_listener_test.go github.com/quic-go/quic-go/http3 QUICEarlyListener"

View File

@ -17,7 +17,6 @@
"github.com/quic-go/qpack"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/utils"
)
const bodyCopyBufferSize = 8 * 1024
@ -26,17 +25,14 @@ type requestWriter struct {
mutex sync.Mutex
encoder *qpack.Encoder
headerBuf *bytes.Buffer
logger utils.Logger
}
func newRequestWriter(logger utils.Logger) *requestWriter {
func newRequestWriter() *requestWriter {
headerBuf := &bytes.Buffer{}
encoder := qpack.NewEncoder(headerBuf)
return &requestWriter{
encoder: encoder,
headerBuf: headerBuf,
logger: logger,
}
}
@ -69,6 +65,10 @@ func (w *requestWriter) writeHeaders(wr io.Writer, req *http.Request, gzip bool)
return err
}
func isExtendedConnectRequest(req *http.Request) bool {
return req.Method == http.MethodConnect && req.Proto != "" && req.Proto != "HTTP/1.1"
}
// copied from net/transport.go
// Modified to support Extended CONNECT:
// Contrary to what the godoc for the http.Request says,
@ -87,7 +87,7 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra
}
// http.NewRequest sets this field to HTTP/1.1
isExtendedConnect := req.Method == http.MethodConnect && req.Proto != "" && req.Proto != "HTTP/1.1"
isExtendedConnect := isExtendedConnectRequest(req)
var path string
if req.Method != http.MethodConnect || isExtendedConnect {
@ -215,13 +215,10 @@ func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, tra
// authorityAddr returns a given authority (a host/IP, or host:port / ip:port)
// and returns a host:port. The port 443 is added if needed.
func authorityAddr(scheme string, authority string) (addr string) {
func authorityAddr(authority string) (addr string) {
host, port, err := net.SplitHostPort(authority)
if err != nil { // authority didn't have a port
port = "443"
if scheme == "http" {
port = "80"
}
host = authority
}
if a, err := idna.ToASCII(host); err == nil {

View File

@ -1,95 +1,68 @@
package http3
import (
"bufio"
"bytes"
"fmt"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/qpack"
)
// The HTTPStreamer allows taking over a HTTP/3 stream. The interface is implemented the http.Response.Body.
// On the client side, the stream will be closed for writing, unless the DontCloseRequestStream RoundTripOpt was set.
// When a stream is taken over, it's the caller's responsibility to close the stream.
type HTTPStreamer interface {
HTTPStream() Stream
}
// The maximum length of an encoded HTTP/3 frame header is 16:
// The frame has a type and length field, both QUIC varints (maximum 8 bytes in length)
const frameHeaderLen = 16
// headerWriter wraps the stream, so that the first Write call flushes the header to the stream
type headerWriter struct {
str quic.Stream
header http.Header
status int // status code passed to WriteHeader
written bool
logger utils.Logger
}
// writeHeader encodes and flush header to the stream
func (hw *headerWriter) writeHeader() error {
var headers bytes.Buffer
enc := qpack.NewEncoder(&headers)
enc.WriteField(qpack.HeaderField{Name: ":status", Value: strconv.Itoa(hw.status)})
for k, v := range hw.header {
for index := range v {
enc.WriteField(qpack.HeaderField{Name: strings.ToLower(k), Value: v[index]})
}
}
buf := make([]byte, 0, frameHeaderLen+headers.Len())
buf = (&headersFrame{Length: uint64(headers.Len())}).Append(buf)
hw.logger.Infof("Responding with %d", hw.status)
buf = append(buf, headers.Bytes()...)
_, err := hw.str.Write(buf)
return err
}
// first Write will trigger flushing header
func (hw *headerWriter) Write(p []byte) (int, error) {
if !hw.written {
if err := hw.writeHeader(); err != nil {
return 0, err
}
hw.written = true
}
return hw.str.Write(p)
}
const maxSmallResponseSize = 4096
type responseWriter struct {
*headerWriter
conn quic.Connection
bufferedStr *bufio.Writer
buf []byte
str *stream
contentLen int64 // if handler set valid Content-Length header
numWritten int64 // bytes written
headerWritten bool
isHead bool
conn Connection
header http.Header
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
smallResponseBuf []byte
contentLen int64 // if handler set valid Content-Length header
numWritten int64 // bytes written
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
hijacked bool // set on HTTPStream is called
logger *slog.Logger
}
var (
_ http.ResponseWriter = &responseWriter{}
_ http.Flusher = &responseWriter{}
_ Hijacker = &responseWriter{}
_ HTTPStreamer = &responseWriter{}
)
func newResponseWriter(str quic.Stream, conn quic.Connection, logger utils.Logger) *responseWriter {
hw := &headerWriter{
str: str,
header: http.Header{},
logger: logger,
}
func newResponseWriter(str *stream, conn Connection, isHead bool, logger *slog.Logger) *responseWriter {
return &responseWriter{
headerWriter: hw,
buf: make([]byte, frameHeaderLen),
conn: conn,
bufferedStr: bufio.NewWriter(hw),
str: str,
conn: conn,
header: http.Header{},
buf: make([]byte, frameHeaderLen),
isHead: isHead,
logger: logger,
}
}
@ -98,7 +71,7 @@ func (w *responseWriter) Header() http.Header {
}
func (w *responseWriter) WriteHeader(status int) {
if w.headerWritten {
if w.headerComplete {
return
}
@ -106,51 +79,57 @@ func (w *responseWriter) WriteHeader(status int) {
if status < 100 || status > 999 {
panic(fmt.Sprintf("invalid WriteHeader code %v", status))
}
if status >= 200 {
w.headerWritten = true
// Add Date header.
// This is what the standard library does.
// Can be disabled by setting the Date header to nil.
if _, ok := w.header["Date"]; !ok {
w.header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
}
// Content-Length checking
// use ParseUint instead of ParseInt, as negative values are invalid
if clen := w.header.Get("Content-Length"); clen != "" {
if cl, err := strconv.ParseUint(clen, 10, 63); err == nil {
w.contentLen = int64(cl)
} else {
// emit a warning for malformed Content-Length and remove it
w.logger.Errorf("Malformed Content-Length %s", clen)
w.header.Del("Content-Length")
}
}
}
w.status = status
if !w.headerWritten {
w.writeHeader()
// immediately write 1xx headers
if status < 200 {
w.writeHeader(status)
return
}
// We're done with headers once we write a status >= 200.
w.headerComplete = true
// Add Date header.
// This is what the standard library does.
// Can be disabled by setting the Date header to nil.
if _, ok := w.header["Date"]; !ok {
w.header.Set("Date", time.Now().UTC().Format(http.TimeFormat))
}
// Content-Length checking
// use ParseUint instead of ParseInt, as negative values are invalid
if clen := w.header.Get("Content-Length"); clen != "" {
if cl, err := strconv.ParseUint(clen, 10, 63); err == nil {
w.contentLen = int64(cl)
} else {
// emit a warning for malformed Content-Length and remove it
logger := w.logger
if logger == nil {
logger = slog.Default()
}
logger.Error("Malformed Content-Length", "value", clen)
w.header.Del("Content-Length")
}
}
}
func (w *responseWriter) sniffContentType(p []byte) {
// If no content type, apply sniffing algorithm to body.
// 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") != ""
hasCE := w.header.Get("Content-Encoding") != ""
if !hasCE && !haveType && !hasTE && len(p) > 0 {
w.header.Set("Content-Type", http.DetectContentType(p))
}
}
func (w *responseWriter) Write(p []byte) (int, error) {
bodyAllowed := bodyAllowedForStatus(w.status)
if !w.headerWritten {
// If body is not allowed, we don't need to (and we can't) sniff the content type.
if bodyAllowed {
// If no content type, apply sniffing algorithm to body.
// We can't use `w.header.Get` here since if the Content-Type was set to nil, we shoundn'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") != ""
hasCE := w.header.Get("Content-Encoding") != ""
if !hasCE && !haveType && !hasTE && len(p) > 0 {
w.header.Set("Content-Type", http.DetectContentType(p))
}
}
if !w.headerComplete {
w.sniffContentType(p)
w.WriteHeader(http.StatusOK)
bodyAllowed = true
}
@ -167,36 +146,101 @@ func (w *responseWriter) Write(p []byte) (int, error) {
return len(p), nil
}
df := &dataFrame{Length: uint64(len(p))}
if !w.headerWritten {
// Buffer small responses.
// This allows us to automatically set the Content-Length field.
if len(w.smallResponseBuf)+len(p) < maxSmallResponseSize {
w.smallResponseBuf = append(w.smallResponseBuf, p...)
return len(p), nil
}
}
return w.doWrite(p)
}
func (w *responseWriter) doWrite(p []byte) (int, error) {
if !w.headerWritten {
w.sniffContentType(w.smallResponseBuf)
if err := w.writeHeader(w.status); err != nil {
return 0, maybeReplaceError(err)
}
w.headerWritten = true
}
l := uint64(len(w.smallResponseBuf) + len(p))
if l == 0 {
return 0, nil
}
df := &dataFrame{Length: l}
w.buf = w.buf[:0]
w.buf = df.Append(w.buf)
if _, err := w.bufferedStr.Write(w.buf); err != nil {
if _, err := w.str.writeUnframed(w.buf); err != nil {
return 0, maybeReplaceError(err)
}
n, err := w.bufferedStr.Write(p)
return n, maybeReplaceError(err)
if len(w.smallResponseBuf) > 0 {
if _, err := w.str.writeUnframed(w.smallResponseBuf); err != nil {
return 0, maybeReplaceError(err)
}
w.smallResponseBuf = nil
}
var n int
if len(p) > 0 {
var err error
n, err = w.str.writeUnframed(p)
if err != nil {
return n, maybeReplaceError(err)
}
}
return n, nil
}
func (w *responseWriter) writeHeader(status int) error {
var headers bytes.Buffer
enc := qpack.NewEncoder(&headers)
if err := enc.WriteField(qpack.HeaderField{Name: ":status", Value: strconv.Itoa(status)}); err != nil {
return err
}
for k, v := range w.header {
for index := range v {
if err := enc.WriteField(qpack.HeaderField{Name: strings.ToLower(k), Value: v[index]}); err != nil {
return err
}
}
}
buf := make([]byte, 0, frameHeaderLen+headers.Len())
buf = (&headersFrame{Length: uint64(headers.Len())}).Append(buf)
buf = append(buf, headers.Bytes()...)
_, err := w.str.writeUnframed(buf)
return err
}
func (w *responseWriter) FlushError() error {
if !w.headerWritten {
if !w.headerComplete {
w.WriteHeader(http.StatusOK)
}
if !w.written {
if err := w.writeHeader(); err != nil {
return maybeReplaceError(err)
}
w.written = true
}
return w.bufferedStr.Flush()
_, err := w.doWrite(nil)
return err
}
func (w *responseWriter) Flush() {
if err := w.FlushError(); err != nil {
w.logger.Errorf("could not flush to stream: %s", err.Error())
if w.logger != nil {
w.logger.Debug("could not flush to stream", "error", err)
}
}
}
func (w *responseWriter) StreamCreator() StreamCreator {
func (w *responseWriter) HTTPStream() Stream {
w.hijacked = true
w.Flush()
return w.str
}
func (w *responseWriter) wasStreamHijacked() bool { return w.hijacked }
func (w *responseWriter) Connection() Connection {
return w.conn
}

View File

@ -15,12 +15,13 @@
"golang.org/x/net/http/httpguts"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/protocol"
)
// Settings are HTTP/3 settings that apply to the underlying connection.
type Settings struct {
// Support for HTTP/3 datagrams (RFC 9297)
EnableDatagram bool
EnableDatagrams bool
// Extended CONNECT, RFC 9220
EnableExtendedConnect bool
// Other settings, defined by the application
@ -32,69 +33,43 @@ type RoundTripOpt struct {
// OnlyCachedConn controls whether the RoundTripper may create a new QUIC connection.
// If set true and no cached connection is available, RoundTripOpt will return ErrNoCachedConn.
OnlyCachedConn bool
// DontCloseRequestStream controls whether the request stream is closed after sending the request.
// If set, context cancellations have no effect after the response headers are received.
DontCloseRequestStream bool
// CheckSettings is run before the request is sent to the server.
// If not yet received, it blocks until the server's SETTINGS frame is received.
// If an error is returned, the request won't be sent to the server, and the error is returned.
CheckSettings func(Settings) error
}
type roundTripCloser interface {
RoundTripOpt(*http.Request, RoundTripOpt) (*http.Response, error)
HandshakeComplete() bool
io.Closer
type singleRoundTripper interface {
OpenRequestStream(context.Context) (RequestStream, error)
RoundTrip(*http.Request) (*http.Response, error)
}
type roundTripCloserWithCount struct {
roundTripCloser
type roundTripperWithCount struct {
cancel context.CancelFunc
dialing chan struct{} // closed as soon as quic.Dial(Early) returned
dialErr error
conn quic.EarlyConnection
rt singleRoundTripper
useCount atomic.Int64
}
func (r *roundTripperWithCount) Close() error {
r.cancel()
<-r.dialing
if r.conn != nil {
return r.conn.CloseWithError(0, "")
}
return nil
}
// RoundTripper implements the http.RoundTripper interface
type RoundTripper struct {
mutex sync.Mutex
// DisableCompression, if true, prevents the Transport from
// requesting compression with an "Accept-Encoding: gzip"
// request header when the Request contains no existing
// Accept-Encoding value. If the Transport requests gzip on
// its own and gets a gzipped response, it's transparently
// decoded in the Response.Body. However, if the user
// explicitly requested gzip it is not automatically
// uncompressed.
DisableCompression bool
// TLSClientConfig specifies the TLS configuration to use with
// tls.Client. If nil, the default configuration is used.
TLSClientConfig *tls.Config
// QuicConfig is the quic.Config used for dialing new connections.
// QUICConfig is the quic.Config used for dialing new connections.
// If nil, reasonable default values will be used.
QuicConfig *quic.Config
// Enable support for HTTP/3 datagrams (RFC 9297).
// If a QuicConfig is set, datagram support also needs to be enabled on the QUIC layer by setting EnableDatagrams.
EnableDatagrams bool
// Additional HTTP/3 settings.
// It is invalid to specify any settings defined by the HTTP/3 draft and the datagram draft.
AdditionalSettings map[uint64]uint64
// When set, this callback is called for the first unknown frame parsed on a bidirectional stream.
// It is called right after parsing the frame type.
// If parsing the frame type fails, the error is passed to the callback.
// In that case, the frame type will not be set.
// Callers can either ignore the frame and return control of the stream back to HTTP/3
// (by returning hijacked false).
// Alternatively, callers can take over the QUIC stream (by returning hijacked true).
StreamHijacker func(FrameType, quic.Connection, quic.Stream, error) (hijacked bool, err error)
// When set, this callback is called for unknown unidirectional stream of unknown stream type.
// If parsing the stream type fails, the error is passed to the callback.
// In that case, the stream type will not be set.
UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream, error) (hijacked bool)
QUICConfig *quic.Config
// Dial specifies an optional dial function for creating QUIC
// connections for requests.
@ -102,13 +77,32 @@ type RoundTripper struct {
// and will be reused for subsequent connections to other servers.
Dial func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error)
// Enable support for HTTP/3 datagrams (RFC 9297).
// If a QUICConfig is set, datagram support also needs to be enabled on the QUIC layer by setting EnableDatagrams.
EnableDatagrams bool
// Additional HTTP/3 settings.
// It is invalid to specify any settings defined by RFC 9114 (HTTP/3) and RFC 9297 (HTTP Datagrams).
AdditionalSettings map[uint64]uint64
// MaxResponseHeaderBytes specifies a limit on how many response bytes are
// allowed in the server's response header.
// Zero means to use a default limit.
MaxResponseHeaderBytes int64
newClient func(hostname string, tlsConf *tls.Config, opts *roundTripperOpts, conf *quic.Config, dialer dialFunc) (roundTripCloser, error) // so we can mock it in tests
clients map[string]*roundTripCloserWithCount
// DisableCompression, if true, prevents the Transport from requesting compression with an
// "Accept-Encoding: gzip" request header when the Request contains no existing Accept-Encoding value.
// If the Transport requests gzip on its own and gets a gzipped response, it's transparently
// decoded in the Response.Body.
// However, if the user explicitly requested gzip it is not automatically uncompressed.
DisableCompression bool
initOnce sync.Once
initErr error
newClient func(quic.EarlyConnection) singleRoundTripper
clients map[string]*roundTripperWithCount
transport *quic.Transport
}
@ -122,6 +116,11 @@ type RoundTripper struct {
// RoundTripOpt is like RoundTrip, but takes options.
func (r *RoundTripper) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
r.initOnce.Do(func() { r.initErr = r.init() })
if r.initErr != nil {
return nil, r.initErr
}
if req.URL == nil {
closeRequestBody(req)
return nil, errors.New("http3: nil Request.URL")
@ -154,15 +153,31 @@ func (r *RoundTripper) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.
return nil, fmt.Errorf("http3: invalid method %q", req.Method)
}
hostname := authorityAddr("https", hostnameFromRequest(req))
cl, isReused, err := r.getClient(hostname, opt.OnlyCachedConn)
hostname := authorityAddr(hostnameFromURL(req.URL))
cl, isReused, err := r.getClient(req.Context(), hostname, opt.OnlyCachedConn)
if err != nil {
return nil, err
}
select {
case <-cl.dialing:
case <-req.Context().Done():
return nil, context.Cause(req.Context())
}
if cl.dialErr != nil {
return nil, cl.dialErr
}
defer cl.useCount.Add(-1)
rsp, err := cl.RoundTripOpt(req, opt)
rsp, err := cl.rt.RoundTrip(req)
if err != nil {
r.removeClient(hostname)
// non-nil errors on roundtrip are likely due to a problem with the connection
// so we remove the client from the cache so that subsequent trips reconnect
// context cancelation is excluded as is does not signify a connection error
if !errors.Is(err, context.Canceled) {
r.removeClient(hostname)
}
if isReused {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return r.RoundTripOpt(req, opt)
@ -177,59 +192,126 @@ func (r *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r.RoundTripOpt(req, RoundTripOpt{})
}
func (r *RoundTripper) getClient(hostname string, onlyCached bool) (rtc *roundTripCloserWithCount, isReused bool, err error) {
func (r *RoundTripper) init() error {
if r.newClient == nil {
r.newClient = func(conn quic.EarlyConnection) singleRoundTripper {
return &SingleDestinationRoundTripper{
Connection: conn,
EnableDatagrams: r.EnableDatagrams,
DisableCompression: r.DisableCompression,
AdditionalSettings: r.AdditionalSettings,
MaxResponseHeaderBytes: r.MaxResponseHeaderBytes,
}
}
}
if r.QUICConfig == nil {
r.QUICConfig = defaultQuicConfig.Clone()
r.QUICConfig.EnableDatagrams = r.EnableDatagrams
}
if r.EnableDatagrams && !r.QUICConfig.EnableDatagrams {
return errors.New("HTTP Datagrams enabled, but QUIC Datagrams disabled")
}
if len(r.QUICConfig.Versions) == 0 {
r.QUICConfig = r.QUICConfig.Clone()
r.QUICConfig.Versions = []quic.Version{protocol.SupportedVersions[0]}
}
if len(r.QUICConfig.Versions) != 1 {
return errors.New("can only use a single QUIC version for dialing a HTTP/3 connection")
}
if r.QUICConfig.MaxIncomingStreams == 0 {
r.QUICConfig.MaxIncomingStreams = -1 // don't allow any bidirectional streams
}
return nil
}
func (r *RoundTripper) getClient(ctx context.Context, hostname string, onlyCached bool) (rtc *roundTripperWithCount, isReused bool, err error) {
r.mutex.Lock()
defer r.mutex.Unlock()
if r.clients == nil {
r.clients = make(map[string]*roundTripCloserWithCount)
r.clients = make(map[string]*roundTripperWithCount)
}
client, ok := r.clients[hostname]
cl, ok := r.clients[hostname]
if !ok {
if onlyCached {
return nil, false, ErrNoCachedConn
}
var err error
newCl := newClient
if r.newClient != nil {
newCl = r.newClient
ctx, cancel := context.WithCancel(ctx)
cl = &roundTripperWithCount{
dialing: make(chan struct{}),
cancel: cancel,
}
dial := r.Dial
if dial == nil {
if r.transport == nil {
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, false, err
}
r.transport = &quic.Transport{Conn: udpConn}
go func() {
defer close(cl.dialing)
defer cancel()
conn, rt, err := r.dial(ctx, hostname)
if err != nil {
cl.dialErr = err
return
}
dial = r.makeDialer()
}
c, err := newCl(
hostname,
r.TLSClientConfig,
&roundTripperOpts{
EnableDatagram: r.EnableDatagrams,
DisableCompression: r.DisableCompression,
MaxHeaderBytes: r.MaxResponseHeaderBytes,
StreamHijacker: r.StreamHijacker,
UniStreamHijacker: r.UniStreamHijacker,
AdditionalSettings: r.AdditionalSettings,
},
r.QuicConfig,
dial,
)
if err != nil {
return nil, false, err
}
client = &roundTripCloserWithCount{roundTripCloser: c}
r.clients[hostname] = client
} else if client.HandshakeComplete() {
isReused = true
cl.conn = conn
cl.rt = rt
}()
r.clients[hostname] = cl
}
client.useCount.Add(1)
return client, isReused, nil
select {
case <-cl.dialing:
if cl.dialErr != nil {
return nil, false, cl.dialErr
}
select {
case <-cl.conn.HandshakeComplete():
isReused = true
default:
}
default:
}
cl.useCount.Add(1)
return cl, isReused, nil
}
func (r *RoundTripper) dial(ctx context.Context, hostname string) (quic.EarlyConnection, singleRoundTripper, error) {
var tlsConf *tls.Config
if r.TLSClientConfig == nil {
tlsConf = &tls.Config{}
} else {
tlsConf = r.TLSClientConfig.Clone()
}
if tlsConf.ServerName == "" {
sni, _, err := net.SplitHostPort(hostname)
if err != nil {
// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
sni = hostname
}
tlsConf.ServerName = sni
}
// Replace existing ALPNs by H3
tlsConf.NextProtos = []string{versionToALPN(r.QUICConfig.Versions[0])}
dial := r.Dial
if dial == nil {
if r.transport == nil {
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, nil, err
}
r.transport = &quic.Transport{Conn: udpConn}
}
dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
return r.transport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
}
}
conn, err := dial(ctx, hostname, tlsConf, r.QUICConfig)
if err != nil {
return nil, nil, err
}
return conn, r.newClient(conn), nil
}
func (r *RoundTripper) removeClient(hostname string) {
@ -246,8 +328,8 @@ func (r *RoundTripper) removeClient(hostname string) {
func (r *RoundTripper) Close() error {
r.mutex.Lock()
defer r.mutex.Unlock()
for _, client := range r.clients {
if err := client.Close(); err != nil {
for _, cl := range r.clients {
if err := cl.Close(); err != nil {
return err
}
}
@ -292,23 +374,12 @@ func isNotToken(r rune) bool {
return !httpguts.IsTokenRune(r)
}
// makeDialer makes a QUIC dialer using r.udpConn.
func (r *RoundTripper) makeDialer() func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
return func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
return r.transport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
}
}
func (r *RoundTripper) CloseIdleConnections() {
r.mutex.Lock()
defer r.mutex.Unlock()
for hostname, client := range r.clients {
if client.useCount.Load() == 0 {
client.Close()
for hostname, cl := range r.clients {
if cl.useCount.Load() == 0 {
cl.Close()
delete(r.clients, hostname)
}
}

View File

@ -6,18 +6,17 @@
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/quicvarint"
"github.com/quic-go/qpack"
@ -31,7 +30,6 @@
quicListenAddr = func(addr string, tlsConf *tls.Config, config *quic.Config) (QUICEarlyListener, error) {
return quic.ListenAddrEarly(addr, tlsConf, config)
}
errPanicked = errors.New("panicked")
)
// NextProtoH3 is the ALPN protocol negotiated during the TLS handshake, for QUIC v1 and v2.
@ -127,20 +125,6 @@ func (k *contextKey) String() string { return "quic-go/http3 context value " + k
// than its string representation.
var RemoteAddrContextKey = &contextKey{"remote-addr"}
type requestError struct {
err error
streamErr ErrCode
connErr ErrCode
}
func newStreamError(code ErrCode, err error) requestError {
return requestError{err: err, streamErr: code}
}
func newConnError(code ErrCode, err error) requestError {
return requestError{err: err, connErr: code}
}
// listenerInfo contains info about specific listener added with addListener
type listenerInfo struct {
port int // 0 means that no info about port is available
@ -157,10 +141,10 @@ type Server struct {
//
// Otherwise, if Port is not set and underlying QUIC listeners do not
// have valid port numbers, the port part is used in Alt-Svc headers set
// with SetQuicHeaders.
// with SetQUICHeaders.
Addr string
// Port is used in Alt-Svc response headers set with SetQuicHeaders. If
// Port is used in Alt-Svc response headers set with SetQUICHeaders. If
// needed Port can be manually set when the Server is created.
//
// This is useful when a Layer 4 firewall is redirecting UDP traffic and
@ -172,20 +156,18 @@ type Server struct {
// set for ListenAndServe and Serve methods.
TLSConfig *tls.Config
// QuicConfig provides the parameters for QUIC connection created with
// Serve. If nil, it uses reasonable default values.
// QUICConfig provides the parameters for QUIC connection created with Serve.
// If nil, it uses reasonable default values.
//
// Configured versions are also used in Alt-Svc response header set with
// SetQuicHeaders.
QuicConfig *quic.Config
// Configured versions are also used in Alt-Svc response header set with SetQUICHeaders.
QUICConfig *quic.Config
// Handler is the HTTP request handler to use. If not set, defaults to
// http.NotFound.
Handler http.Handler
// EnableDatagrams enables support for HTTP/3 datagrams.
// If set to true, QuicConfig.EnableDatagram will be set.
// See https://datatracker.ietf.org/doc/html/rfc9297.
// EnableDatagrams enables support for HTTP/3 datagrams (RFC 9297).
// If set to true, QUICConfig.EnableDatagrams will be set.
EnableDatagrams bool
// MaxHeaderBytes controls the maximum number of bytes the server will
@ -195,7 +177,7 @@ type Server struct {
MaxHeaderBytes int
// AdditionalSettings specifies additional HTTP/3 settings.
// It is invalid to specify any settings defined by the HTTP/3 draft and the datagram draft.
// It is invalid to specify any settings defined by RFC 9114 (HTTP/3) and RFC 9297 (HTTP Datagrams).
AdditionalSettings map[uint64]uint64
// StreamHijacker, when set, is called for the first unknown frame parsed on a bidirectional stream.
@ -205,26 +187,26 @@ type Server struct {
// Callers can either ignore the frame and return control of the stream back to HTTP/3
// (by returning hijacked false).
// Alternatively, callers can take over the QUIC stream (by returning hijacked true).
StreamHijacker func(FrameType, quic.Connection, quic.Stream, error) (hijacked bool, err error)
StreamHijacker func(FrameType, quic.ConnectionTracingID, quic.Stream, error) (hijacked bool, err error)
// UniStreamHijacker, when set, is called for unknown unidirectional stream of unknown stream type.
// If parsing the stream type fails, the error is passed to the callback.
// In that case, the stream type will not be set.
UniStreamHijacker func(StreamType, quic.Connection, quic.ReceiveStream, error) (hijacked bool)
UniStreamHijacker func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)
// ConnContext optionally specifies a function that modifies
// the context used for a new connection c. The provided ctx
// has a ServerContextKey value.
ConnContext func(ctx context.Context, c quic.Connection) context.Context
Logger *slog.Logger
mutex sync.RWMutex
listeners map[*QUICEarlyListener]listenerInfo
closed bool
altSvcHeader string
logger utils.Logger
}
// ListenAndServe listens on the UDP address s.Addr and calls s.Handler to handle HTTP/3 requests on incoming connections.
@ -261,12 +243,6 @@ func (s *Server) Serve(conn net.PacketConn) error {
// ServeQUICConn serves a single QUIC connection.
func (s *Server) ServeQUICConn(conn quic.Connection) error {
s.mutex.Lock()
if s.logger == nil {
s.logger = utils.DefaultLogger.WithPrefix("server")
}
s.mutex.Unlock()
return s.handleConn(conn)
}
@ -290,7 +266,9 @@ func (s *Server) ServeListener(ln QUICEarlyListener) error {
}
go func() {
if err := s.handleConn(conn); err != nil {
s.logger.Debugf("handling connection failed: %s", err)
if s.Logger != nil {
s.Logger.Debug("handling connection failed", "error", err)
}
}
}()
}
@ -311,11 +289,11 @@ func (s *Server) serveConn(tlsConf *tls.Config, conn net.PacketConn) error {
}
baseConf := ConfigureTLSConfig(tlsConf)
quicConf := s.QuicConfig
quicConf := s.QUICConfig
if quicConf == nil {
quicConf = &quic.Config{Allow0RTT: true}
} else {
quicConf = s.QuicConfig.Clone()
quicConf = s.QUICConfig.Clone()
}
if s.EnableDatagrams {
quicConf.EnableDatagrams = true
@ -360,8 +338,8 @@ func (s *Server) generateAltSvcHeader() {
// This code assumes that we will use protocol.SupportedVersions if no quic.Config is passed.
supportedVersions := protocol.SupportedVersions
if s.QuicConfig != nil && len(s.QuicConfig.Versions) > 0 {
supportedVersions = s.QuicConfig.Versions
if s.QUICConfig != nil && len(s.QUICConfig.Versions) > 0 {
supportedVersions = s.QUICConfig.Versions
}
// keep track of which have been seen so we don't yield duplicate values
@ -417,9 +395,6 @@ func (s *Server) addListener(l *QUICEarlyListener) error {
if s.closed {
return http.ErrServerClosed
}
if s.logger == nil {
s.logger = utils.DefaultLogger.WithPrefix("server")
}
if s.listeners == nil {
s.listeners = make(map[*QUICEarlyListener]listenerInfo)
}
@ -428,7 +403,11 @@ func (s *Server) addListener(l *QUICEarlyListener) error {
if port, err := extractPort(laddr.String()); err == nil {
s.listeners[l] = listenerInfo{port}
} else {
s.logger.Errorf("Unable to extract port from listener %s, will not be announced using SetQuicHeaders: %s", laddr, err)
logger := s.Logger
if logger == nil {
logger = slog.Default()
}
logger.Error("Unable to extract port from listener, will not be announced using SetQUICHeaders", "local addr", laddr, "error", err)
s.listeners[l] = listenerInfo{}
}
s.generateAltSvcHeader()
@ -443,8 +422,6 @@ func (s *Server) removeListener(l *QUICEarlyListener) {
}
func (s *Server) handleConn(conn quic.Connection) error {
decoder := qpack.NewDecoder(nil)
// send a SETTINGS frame
str, err := conn.OpenUniStream()
if err != nil {
@ -459,12 +436,17 @@ func (s *Server) handleConn(conn quic.Connection) error {
}).Append(b)
str.Write(b)
go s.handleUnidirectionalStreams(conn)
hconn := newConnection(
conn,
s.EnableDatagrams,
protocol.PerspectiveServer,
s.Logger,
)
go hconn.HandleUnidirectionalStreams(s.UniStreamHijacker)
// Process all requests immediately.
// It's the client's responsibility to decide which requests are eligible for 0-RTT.
for {
str, err := conn.AcceptStream(context.Background())
str, datagrams, err := hconn.acceptStream(context.Background())
if err != nil {
var appErr *quic.ApplicationError
if errors.As(err, &appErr) && appErr.ErrorCode == quic.ApplicationErrorCode(ErrCodeNoError) {
@ -472,93 +454,7 @@ func (s *Server) handleConn(conn quic.Connection) error {
}
return fmt.Errorf("accepting stream failed: %w", err)
}
go func() {
rerr := s.handleRequest(conn, str, decoder, func() {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "")
})
if rerr.err == errHijacked {
return
}
if rerr.err != nil || rerr.streamErr != 0 || rerr.connErr != 0 {
s.logger.Debugf("Handling request failed: %s", err)
if rerr.streamErr != 0 {
str.CancelWrite(quic.StreamErrorCode(rerr.streamErr))
}
if rerr.connErr != 0 {
var reason string
if rerr.err != nil {
reason = rerr.err.Error()
}
conn.CloseWithError(quic.ApplicationErrorCode(rerr.connErr), reason)
}
return
}
str.Close()
}()
}
}
func (s *Server) handleUnidirectionalStreams(conn quic.Connection) {
var rcvdControlStream atomic.Bool
for {
str, err := conn.AcceptUniStream(context.Background())
if err != nil {
s.logger.Debugf("accepting unidirectional stream failed: %s", err)
return
}
go func(str quic.ReceiveStream) {
streamType, err := quicvarint.Read(quicvarint.NewReader(str))
if err != nil {
if s.UniStreamHijacker != nil && s.UniStreamHijacker(StreamType(streamType), conn, str, err) {
return
}
s.logger.Debugf("reading stream type on stream %d failed: %s", str.StreamID(), err)
return
}
// We're only interested in the control stream here.
switch streamType {
case streamTypeControlStream:
case streamTypeQPACKEncoderStream, streamTypeQPACKDecoderStream:
// Our QPACK implementation doesn't use the dynamic table yet.
// TODO: check that only one stream of each type is opened.
return
case streamTypePushStream: // only the server can push
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "")
return
default:
if s.UniStreamHijacker != nil && s.UniStreamHijacker(StreamType(streamType), conn, str, nil) {
return
}
str.CancelRead(quic.StreamErrorCode(ErrCodeStreamCreationError))
return
}
// Only a single control stream is allowed.
if isFirstControlStr := rcvdControlStream.CompareAndSwap(false, true); !isFirstControlStr {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate control stream")
return
}
f, err := parseNextFrame(str, nil)
if err != nil {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
return
}
sf, ok := f.(*settingsFrame)
if !ok {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
return
}
if !sf.Datagram {
return
}
// If datagram support was enabled on our side as well as on the client side,
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
if s.EnableDatagrams && !conn.ConnectionState().SupportsDatagrams {
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
}
}(str)
go s.handleRequest(hconn, str, datagrams, hconn.decoder)
}
}
@ -569,37 +465,53 @@ func (s *Server) maxHeaderBytes() uint64 {
return uint64(s.MaxHeaderBytes)
}
func (s *Server) handleRequest(conn quic.Connection, str quic.Stream, decoder *qpack.Decoder, onFrameError func()) requestError {
func (s *Server) handleRequest(conn *connection, str quic.Stream, datagrams *datagrammer, decoder *qpack.Decoder) {
var ufh unknownFrameHandlerFunc
if s.StreamHijacker != nil {
ufh = func(ft FrameType, e error) (processed bool, err error) { return s.StreamHijacker(ft, conn, str, e) }
ufh = func(ft FrameType, e error) (processed bool, err error) {
return s.StreamHijacker(
ft,
conn.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID),
str,
e,
)
}
}
frame, err := parseNextFrame(str, ufh)
if err != nil {
if err == errHijacked {
return requestError{err: errHijacked}
if !errors.Is(err, errHijacked) {
str.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
}
return newStreamError(ErrCodeRequestIncomplete, err)
return
}
hf, ok := frame.(*headersFrame)
if !ok {
return newConnError(ErrCodeFrameUnexpected, errors.New("expected first frame to be a HEADERS frame"))
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "expected first frame to be a HEADERS frame")
return
}
if hf.Length > s.maxHeaderBytes() {
return newStreamError(ErrCodeFrameError, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", hf.Length, s.maxHeaderBytes()))
str.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
str.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
return
}
headerBlock := make([]byte, hf.Length)
if _, err := io.ReadFull(str, headerBlock); err != nil {
return newStreamError(ErrCodeRequestIncomplete, err)
str.CancelRead(quic.StreamErrorCode(ErrCodeRequestIncomplete))
str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestIncomplete))
return
}
hfs, err := decoder.DecodeFull(headerBlock)
if err != nil {
// TODO: use the right error code
return newConnError(ErrCodeGeneralProtocolError, err)
conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeGeneralProtocolError), "expected first frame to be a HEADERS frame")
return
}
req, err := requestFromHeaders(hfs)
if err != nil {
return newStreamError(ErrCodeMessageError, err)
str.CancelRead(quic.StreamErrorCode(ErrCodeMessageError))
str.CancelWrite(quic.StreamErrorCode(ErrCodeMessageError))
return
}
connState := conn.ConnectionState().TLS
@ -608,19 +520,16 @@ func (s *Server) handleRequest(conn quic.Connection, str quic.Stream, decoder *q
// Check that the client 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.
var httpStr Stream
contentLength := int64(-1)
if _, ok := req.Header["Content-Length"]; ok && req.ContentLength >= 0 {
httpStr = newLengthLimitedStream(newStream(str, onFrameError), req.ContentLength)
} else {
httpStr = newStream(str, onFrameError)
contentLength = req.ContentLength
}
body := newRequestBody(httpStr)
hstr := newStream(str, conn, datagrams)
body := newRequestBody(hstr, contentLength, conn.Context(), conn.ReceivedSettings(), conn.Settings)
req.Body = body
if s.logger.Debug() {
s.logger.Infof("%s %s%s, on stream %d", req.Method, req.Host, req.RequestURI, str.StreamID())
} else {
s.logger.Infof("%s %s%s", req.Method, req.Host, req.RequestURI)
if s.Logger != nil {
s.Logger.Debug("handling request", "method", req.Method, "host", req.Host, "uri", req.RequestURI)
}
ctx := str.Context()
@ -634,15 +543,13 @@ func (s *Server) handleRequest(conn quic.Connection, str quic.Stream, decoder *q
}
}
req = req.WithContext(ctx)
r := newResponseWriter(str, conn, s.logger)
if req.Method == http.MethodHead {
r.isHead = true
}
r := newResponseWriter(hstr, conn, req.Method == http.MethodHead, s.Logger)
handler := s.Handler
if handler == nil {
handler = http.DefaultServeMux
}
// It's the client's responsibility to decide which requests are eligible for 0-RTT.
var panicked bool
func() {
defer func() {
@ -655,34 +562,42 @@ func() {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
s.logger.Errorf("http: panic serving: %v\n%s", p, buf)
logger := s.Logger
if logger == nil {
logger = slog.Default()
}
logger.Error("http: panic serving", "arg", p, "trace", buf)
}
}()
handler.ServeHTTP(r, req)
}()
if body.wasStreamHijacked() {
return requestError{err: errHijacked}
if r.wasStreamHijacked() {
return
}
// only write response when there is no panic
if !panicked {
// response not written to the client yet, set Content-Length
if !r.written {
if !r.headerWritten {
if _, haveCL := r.header["Content-Length"]; !haveCL {
r.header.Set("Content-Length", strconv.FormatInt(r.numWritten, 10))
}
}
r.Flush()
}
// If the EOF was read by the handler, CancelRead() is a no-op.
str.CancelRead(quic.StreamErrorCode(ErrCodeNoError))
// abort the stream when there is a panic
if panicked {
return newStreamError(ErrCodeInternalError, errPanicked)
str.CancelRead(quic.StreamErrorCode(ErrCodeInternalError))
str.CancelWrite(quic.StreamErrorCode(ErrCodeInternalError))
return
}
return requestError{}
// If the EOF was read by the handler, CancelRead() is a no-op.
str.CancelRead(quic.StreamErrorCode(ErrCodeNoError))
str.Close()
}
// Close the server immediately, aborting requests and sending CONNECTION_CLOSE frames to connected clients.
@ -709,32 +624,36 @@ func (s *Server) CloseGracefully(timeout time.Duration) error {
return nil
}
// ErrNoAltSvcPort is the error returned by SetQuicHeaders when no port was found
// ErrNoAltSvcPort is the error returned by SetQUICHeaders when no port was found
// for Alt-Svc to announce. This can happen if listening on a PacketConn without a port
// (UNIX socket, for example) and no port is specified in Server.Port or Server.Addr.
var ErrNoAltSvcPort = errors.New("no port can be announced, specify it explicitly using Server.Port or Server.Addr")
// SetQuicHeaders can be used to set the proper headers that announce that this server supports HTTP/3.
// The values set by default advertise all of the ports the server is listening on, but can be
// changed to a specific port by setting Server.Port before launching the serverr.
// SetQUICHeaders can be used to set the proper headers that announce that this server supports HTTP/3.
// The values set by default advertise all the ports the server is listening on, but can be
// changed to a specific port by setting Server.Port before launching the server.
// If no listener's Addr().String() returns an address with a valid port, Server.Addr will be used
// to extract the port, if specified.
// For example, a server launched using ListenAndServe on an address with port 443 would set:
//
// Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
func (s *Server) SetQuicHeaders(hdr http.Header) error {
// Alt-Svc: h3=":443"; ma=2592000
func (s *Server) SetQUICHeaders(hdr http.Header) error {
s.mutex.RLock()
defer s.mutex.RUnlock()
if s.altSvcHeader == "" {
return ErrNoAltSvcPort
}
// use the map directly to avoid constant canonicalization
// since the key is already canonicalized
// use the map directly to avoid constant canonicalization since the key is already canonicalized
hdr["Alt-Svc"] = append(hdr["Alt-Svc"], s.altSvcHeader)
return nil
}
// Deprecated: use SetQUICHeaders instead.
func (s *Server) SetQuicHeaders(hdr http.Header) error {
return s.SetQUICHeaders(hdr)
}
// ListenAndServeQUIC listens on the UDP network address addr and calls the
// handler for HTTP/3 requests on incoming connections. http.DefaultServeMux is
// used when handler is nil.
@ -792,7 +711,7 @@ func ListenAndServe(addr, certFile, keyFile string, handler http.Handler) error
qErr := make(chan error, 1)
go func() {
hErr <- http.ListenAndServeTLS(addr, certFile, keyFile, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
quicServer.SetQuicHeaders(w.Header())
quicServer.SetQUICHeaders(w.Header())
handler.ServeHTTP(w, r)
}))
}()

View File

@ -0,0 +1,91 @@
package http3
import (
"errors"
"sync"
"github.com/quic-go/quic-go"
)
type streamState uint8
const (
streamStateOpen streamState = iota
streamStateReceiveClosed
streamStateSendClosed
streamStateSendAndReceiveClosed
)
type stateTrackingStream struct {
quic.Stream
mx sync.Mutex
state streamState
onStateChange func(streamState, error)
}
func newStateTrackingStream(s quic.Stream, onStateChange func(streamState, error)) *stateTrackingStream {
return &stateTrackingStream{
Stream: s,
state: streamStateOpen,
onStateChange: onStateChange,
}
}
var _ quic.Stream = &stateTrackingStream{}
func (s *stateTrackingStream) closeSend(e error) {
s.mx.Lock()
defer s.mx.Unlock()
if s.state == streamStateReceiveClosed || s.state == streamStateSendAndReceiveClosed {
s.state = streamStateSendAndReceiveClosed
} else {
s.state = streamStateSendClosed
}
s.onStateChange(s.state, e)
}
func (s *stateTrackingStream) closeReceive(e error) {
s.mx.Lock()
defer s.mx.Unlock()
if s.state == streamStateSendClosed || s.state == streamStateSendAndReceiveClosed {
s.state = streamStateSendAndReceiveClosed
} else {
s.state = streamStateReceiveClosed
}
s.onStateChange(s.state, e)
}
func (s *stateTrackingStream) Close() error {
s.closeSend(errors.New("write on closed stream"))
return s.Stream.Close()
}
func (s *stateTrackingStream) CancelWrite(e quic.StreamErrorCode) {
s.closeSend(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e})
s.Stream.CancelWrite(e)
}
func (s *stateTrackingStream) Write(b []byte) (int, error) {
n, err := s.Stream.Write(b)
if err != nil {
s.closeSend(err)
}
return n, err
}
func (s *stateTrackingStream) CancelRead(e quic.StreamErrorCode) {
s.closeReceive(&quic.StreamError{StreamID: s.Stream.StreamID(), ErrorCode: e})
s.Stream.CancelRead(e)
}
func (s *stateTrackingStream) Read(b []byte) (int, error) {
n, err := s.Stream.Read(b)
if err != nil {
s.closeReceive(err)
}
return n, err
}

View File

@ -59,6 +59,9 @@ type TokenStore interface {
// as well as on the context passed to logging.Tracer.NewConnectionTracer.
var ConnectionTracingKey = connTracingCtxKey{}
// ConnectionTracingID is the type of the context value saved under the ConnectionTracingKey.
type ConnectionTracingID uint64
type connTracingCtxKey struct{}
// QUICVersionContextKey can be used to find out the QUIC version of a TLS handshake from the
@ -121,7 +124,9 @@ type SendStream interface {
// CancelWrite aborts sending on this stream.
// Data already written, but not yet delivered to the peer is not guaranteed to be delivered reliably.
// Write will unblock immediately, and future calls to Write will fail.
// When called multiple times or after closing the stream it is a no-op.
// When called multiple times it is a no-op.
// When called after Close, it aborts delivery. Note that there is no guarantee if
// the peer will receive the FIN or the reset first.
CancelWrite(StreamErrorCode)
// The Context is canceled as soon as the write-side of the stream is closed.
// This happens when Close() or CancelWrite() is called, or when the peer

View File

@ -111,6 +111,7 @@ func (c *streamFlowController) AddBytesRead(n protocol.ByteCount) {
func (c *streamFlowController) Abandon() {
c.mutex.Lock()
unread := c.highestReceived - c.bytesRead
c.bytesRead = c.highestReceived
c.mutex.Unlock()
if unread > 0 {
c.connection.AddBytesRead(unread)

View File

@ -3,10 +3,10 @@
import "time"
// DesiredReceiveBufferSize is the kernel UDP receive buffer size that we'd like to use.
const DesiredReceiveBufferSize = (1 << 20) * 2 // 2 MB
const DesiredReceiveBufferSize = (1 << 20) * 7 // 7 MB
// DesiredSendBufferSize is the kernel UDP send buffer size that we'd like to use.
const DesiredSendBufferSize = (1 << 20) * 2 // 2 MB
const DesiredSendBufferSize = (1 << 20) * 7 // 7 MB
// InitialPacketSizeIPv4 is the maximum packet size that we use for sending IPv4 packets.
const InitialPacketSizeIPv4 = 1252

View File

@ -163,7 +163,7 @@ func (f *AckFrame) Length(_ protocol.Version) protocol.ByteCount {
length += quicvarint.Len(f.ECT1)
length += quicvarint.Len(f.ECNCE)
}
return length
return protocol.ByteCount(length)
}
// gets the number of ACK ranges that can be encoded
@ -174,7 +174,7 @@ func (f *AckFrame) numEncodableAckRanges() int {
for i := 1; i < len(f.AckRanges); i++ {
gap, len := f.encodeAckRange(i)
rangeLen := quicvarint.Len(gap) + quicvarint.Len(len)
if length+rangeLen > protocol.MaxAckFrameSize {
if protocol.ByteCount(length+rangeLen) > protocol.MaxAckFrameSize {
// Writing range i would exceed the MaxAckFrameSize.
// So encode one range less than that.
return i - 1

View File

@ -54,9 +54,9 @@ func parseConnectionCloseFrame(r *bytes.Reader, typ uint64, _ protocol.Version)
// Length of a written frame
func (f *ConnectionCloseFrame) Length(protocol.Version) protocol.ByteCount {
length := 1 + quicvarint.Len(f.ErrorCode) + quicvarint.Len(uint64(len(f.ReasonPhrase))) + protocol.ByteCount(len(f.ReasonPhrase))
length := 1 + protocol.ByteCount(quicvarint.Len(f.ErrorCode)+quicvarint.Len(uint64(len(f.ReasonPhrase)))) + protocol.ByteCount(len(f.ReasonPhrase))
if !f.IsApplicationError {
length += quicvarint.Len(f.FrameType) // for the frame type
length += protocol.ByteCount(quicvarint.Len(f.FrameType)) // for the frame type
}
return length
}

View File

@ -48,14 +48,14 @@ func (f *CryptoFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
// Length of a written frame
func (f *CryptoFrame) Length(_ protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.Offset)) + quicvarint.Len(uint64(len(f.Data))) + protocol.ByteCount(len(f.Data))
return protocol.ByteCount(1 + quicvarint.Len(uint64(f.Offset)) + quicvarint.Len(uint64(len(f.Data))) + len(f.Data))
}
// MaxDataLen returns the maximum data length
func (f *CryptoFrame) MaxDataLen(maxSize protocol.ByteCount) protocol.ByteCount {
// pretend that the data size will be 1 bytes
// if it turns out that varint encoding the length will consume 2 bytes, we need to adjust the data length afterwards
headerLen := 1 + quicvarint.Len(uint64(f.Offset)) + 1
headerLen := protocol.ByteCount(1 + quicvarint.Len(uint64(f.Offset)) + 1)
if headerLen > maxSize {
return 0
}

View File

@ -27,5 +27,5 @@ func (f *DataBlockedFrame) Append(b []byte, version protocol.Version) ([]byte, e
// Length of a written frame
func (f *DataBlockedFrame) Length(version protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.MaximumData))
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.MaximumData)))
}

View File

@ -80,7 +80,7 @@ func (f *DatagramFrame) MaxDataLen(maxSize protocol.ByteCount, version protocol.
func (f *DatagramFrame) Length(_ protocol.Version) protocol.ByteCount {
length := 1 + protocol.ByteCount(len(f.Data))
if f.DataLenPresent {
length += quicvarint.Len(uint64(len(f.Data)))
length += protocol.ByteCount(quicvarint.Len(uint64(len(f.Data))))
}
return length
}

View File

@ -165,7 +165,7 @@ func (h *ExtendedHeader) ParsedLen() protocol.ByteCount {
func (h *ExtendedHeader) GetLength(_ protocol.Version) protocol.ByteCount {
length := 1 /* type byte */ + 4 /* version */ + 1 /* dest conn ID len */ + protocol.ByteCount(h.DestConnectionID.Len()) + 1 /* src conn ID len */ + protocol.ByteCount(h.SrcConnectionID.Len()) + protocol.ByteCount(h.PacketNumberLen) + 2 /* length */
if h.Type == protocol.PacketTypeInitial {
length += quicvarint.Len(uint64(len(h.Token))) + protocol.ByteCount(len(h.Token))
length += protocol.ByteCount(quicvarint.Len(uint64(len(h.Token))) + len(h.Token))
}
return length
}

View File

@ -31,5 +31,5 @@ func (f *MaxDataFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
// Length of a written frame
func (f *MaxDataFrame) Length(_ protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.MaximumData))
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.MaximumData)))
}

View File

@ -29,7 +29,7 @@ func parseMaxStreamDataFrame(r *bytes.Reader, _ protocol.Version) (*MaxStreamDat
}, nil
}
func (f *MaxStreamDataFrame) Append(b []byte, version protocol.Version) ([]byte, error) {
func (f *MaxStreamDataFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
b = append(b, maxStreamDataFrameType)
b = quicvarint.Append(b, uint64(f.StreamID))
b = quicvarint.Append(b, uint64(f.MaximumStreamData))
@ -37,6 +37,6 @@ func (f *MaxStreamDataFrame) Append(b []byte, version protocol.Version) ([]byte,
}
// Length of a written frame
func (f *MaxStreamDataFrame) Length(version protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.StreamID)) + quicvarint.Len(uint64(f.MaximumStreamData))
func (f *MaxStreamDataFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.StreamID))+quicvarint.Len(uint64(f.MaximumStreamData)))
}

View File

@ -46,5 +46,5 @@ func (f *MaxStreamsFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
// Length of a written frame
func (f *MaxStreamsFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.MaxStreamNum))
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.MaxStreamNum)))
}

View File

@ -73,5 +73,5 @@ func (f *NewConnectionIDFrame) Append(b []byte, _ protocol.Version) ([]byte, err
// Length of a written frame
func (f *NewConnectionIDFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(f.SequenceNumber) + quicvarint.Len(f.RetirePriorTo) + 1 /* connection ID length */ + protocol.ByteCount(f.ConnectionID.Len()) + 16
return 1 + protocol.ByteCount(quicvarint.Len(f.SequenceNumber)+quicvarint.Len(f.RetirePriorTo)+1 /* connection ID length */ +f.ConnectionID.Len()) + 16
}

View File

@ -41,5 +41,5 @@ func (f *NewTokenFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
// Length of a written frame
func (f *NewTokenFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(len(f.Token))) + protocol.ByteCount(len(f.Token))
return 1 + protocol.ByteCount(quicvarint.Len(uint64(len(f.Token)))+len(f.Token))
}

View File

@ -49,6 +49,6 @@ func (f *ResetStreamFrame) Append(b []byte, _ protocol.Version) ([]byte, error)
}
// Length of a written frame
func (f *ResetStreamFrame) Length(version protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.StreamID)) + quicvarint.Len(uint64(f.ErrorCode)) + quicvarint.Len(uint64(f.FinalSize))
func (f *ResetStreamFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.StreamID))+quicvarint.Len(uint64(f.ErrorCode))+quicvarint.Len(uint64(f.FinalSize)))
}

View File

@ -28,5 +28,5 @@ func (f *RetireConnectionIDFrame) Append(b []byte, _ protocol.Version) ([]byte,
// Length of a written frame
func (f *RetireConnectionIDFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(f.SequenceNumber)
return 1 + protocol.ByteCount(quicvarint.Len(f.SequenceNumber))
}

View File

@ -33,7 +33,7 @@ func parseStopSendingFrame(r *bytes.Reader, _ protocol.Version) (*StopSendingFra
// Length of a written frame
func (f *StopSendingFrame) Length(_ protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.StreamID)) + quicvarint.Len(uint64(f.ErrorCode))
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.StreamID))+quicvarint.Len(uint64(f.ErrorCode)))
}
func (f *StopSendingFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {

View File

@ -37,6 +37,6 @@ func (f *StreamDataBlockedFrame) Append(b []byte, _ protocol.Version) ([]byte, e
}
// Length of a written frame
func (f *StreamDataBlockedFrame) Length(version protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.StreamID)) + quicvarint.Len(uint64(f.MaximumStreamData))
func (f *StreamDataBlockedFrame) Length(protocol.Version) protocol.ByteCount {
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.StreamID))+quicvarint.Len(uint64(f.MaximumStreamData)))
}

View File

@ -108,7 +108,7 @@ func (f *StreamFrame) Append(b []byte, _ protocol.Version) ([]byte, error) {
}
// Length returns the total length of the STREAM frame
func (f *StreamFrame) Length(version protocol.Version) protocol.ByteCount {
func (f *StreamFrame) Length(protocol.Version) protocol.ByteCount {
length := 1 + quicvarint.Len(uint64(f.StreamID))
if f.Offset != 0 {
length += quicvarint.Len(uint64(f.Offset))
@ -116,7 +116,7 @@ func (f *StreamFrame) Length(version protocol.Version) protocol.ByteCount {
if f.DataLenPresent {
length += quicvarint.Len(uint64(f.DataLen()))
}
return length + f.DataLen()
return protocol.ByteCount(length) + f.DataLen()
}
// DataLen gives the length of data in bytes
@ -126,14 +126,14 @@ func (f *StreamFrame) DataLen() protocol.ByteCount {
// MaxDataLen returns the maximum data length
// If 0 is returned, writing will fail (a STREAM frame must contain at least 1 byte of data).
func (f *StreamFrame) MaxDataLen(maxSize protocol.ByteCount, version protocol.Version) protocol.ByteCount {
headerLen := 1 + quicvarint.Len(uint64(f.StreamID))
func (f *StreamFrame) MaxDataLen(maxSize protocol.ByteCount, _ protocol.Version) protocol.ByteCount {
headerLen := 1 + protocol.ByteCount(quicvarint.Len(uint64(f.StreamID)))
if f.Offset != 0 {
headerLen += quicvarint.Len(uint64(f.Offset))
headerLen += protocol.ByteCount(quicvarint.Len(uint64(f.Offset)))
}
if f.DataLenPresent {
// pretend that the data size will be 1 bytes
// if it turns out that varint encoding the length will consume 2 bytes, we need to adjust the data length afterwards
// Pretend that the data size will be 1 byte.
// If it turns out that varint encoding the length will consume 2 bytes, we need to adjust the data length afterward
headerLen++
}
if headerLen > maxSize {

View File

@ -46,5 +46,5 @@ func (f *StreamsBlockedFrame) Append(b []byte, _ protocol.Version) ([]byte, erro
// Length of a written frame
func (f *StreamsBlockedFrame) Length(_ protocol.Version) protocol.ByteCount {
return 1 + quicvarint.Len(uint64(f.StreamLimit))
return 1 + protocol.ByteCount(quicvarint.Len(uint64(f.StreamLimit)))
}

View File

@ -3,8 +3,6 @@
import (
"fmt"
"io"
"github.com/quic-go/quic-go/internal/protocol"
)
// taken from the QUIC draft
@ -91,7 +89,7 @@ func Append(b []byte, i uint64) []byte {
}
// AppendWithLen append i in the QUIC varint format with the desired length.
func AppendWithLen(b []byte, i uint64, length protocol.ByteCount) []byte {
func AppendWithLen(b []byte, i uint64, length int) []byte {
if length != 1 && length != 2 && length != 4 && length != 8 {
panic("invalid varint length")
}
@ -109,17 +107,17 @@ func AppendWithLen(b []byte, i uint64, length protocol.ByteCount) []byte {
} else if length == 8 {
b = append(b, 0b11000000)
}
for j := protocol.ByteCount(1); j < length-l; j++ {
for j := 1; j < length-l; j++ {
b = append(b, 0)
}
for j := protocol.ByteCount(0); j < l; j++ {
for j := 0; j < l; j++ {
b = append(b, uint8(i>>(8*(l-1-j))))
}
return b
}
// Len determines the number of bytes that will be needed to write the number i.
func Len(i uint64) protocol.ByteCount {
func Len(i uint64) int {
if i <= maxVarInt1 {
return 1
}

View File

@ -37,10 +37,14 @@ type receiveStream struct {
readPosInFrame int
currentFrameIsLast bool // is the currentFrame the last frame on this stream
finRead bool // set once we read a frame with a Fin
// Set once we read the io.EOF or the cancellation error.
// Note that for local cancellations, this doesn't necessarily mean that we know the final offset yet.
errorRead bool
completed bool // set once we've called streamSender.onStreamCompleted
cancelledRemotely bool
cancelledLocally bool
cancelErr *StreamError
closeForShutdownErr error
cancelReadErr error
resetRemotelyErr *StreamError
readChan chan struct{}
readOnce chan struct{} // cap: 1, to protect against concurrent use of Read
@ -83,7 +87,8 @@ func (s *receiveStream) Read(p []byte) (int, error) {
defer func() { <-s.readOnce }()
s.mutex.Lock()
completed, n, err := s.readImpl(p)
n, err := s.readImpl(p)
completed := s.isNewlyCompleted()
s.mutex.Unlock()
if completed {
@ -92,18 +97,38 @@ func (s *receiveStream) Read(p []byte) (int, error) {
return n, err
}
func (s *receiveStream) readImpl(p []byte) (bool /*stream completed */, int, error) {
if s.finRead {
return false, 0, io.EOF
func (s *receiveStream) isNewlyCompleted() bool {
if s.completed {
return false
}
if s.cancelReadErr != nil {
return false, 0, s.cancelReadErr
// We need to know the final offset (either via FIN or RESET_STREAM) for flow control accounting.
if s.finalOffset == protocol.MaxByteCount {
return false
}
if s.resetRemotelyErr != nil {
return false, 0, s.resetRemotelyErr
// We're done with the stream if it was cancelled locally...
if s.cancelledLocally {
s.completed = true
return true
}
// ... or if the error (either io.EOF or the reset error) was read
if s.errorRead {
s.completed = true
return true
}
return false
}
func (s *receiveStream) readImpl(p []byte) (int, error) {
if s.currentFrameIsLast && s.currentFrame == nil {
s.errorRead = true
return 0, io.EOF
}
if s.cancelledRemotely || s.cancelledLocally {
s.errorRead = true
return 0, s.cancelErr
}
if s.closeForShutdownErr != nil {
return false, 0, s.closeForShutdownErr
return 0, s.closeForShutdownErr
}
var bytesRead int
@ -113,25 +138,23 @@ func (s *receiveStream) readImpl(p []byte) (bool /*stream completed */, int, err
s.dequeueNextFrame()
}
if s.currentFrame == nil && bytesRead > 0 {
return false, bytesRead, s.closeForShutdownErr
return bytesRead, s.closeForShutdownErr
}
for {
// Stop waiting on errors
if s.closeForShutdownErr != nil {
return false, bytesRead, s.closeForShutdownErr
return bytesRead, s.closeForShutdownErr
}
if s.cancelReadErr != nil {
return false, bytesRead, s.cancelReadErr
}
if s.resetRemotelyErr != nil {
return false, bytesRead, s.resetRemotelyErr
if s.cancelledRemotely || s.cancelledLocally {
s.errorRead = true
return 0, s.cancelErr
}
deadline := s.deadline
if !deadline.IsZero() {
if !time.Now().Before(deadline) {
return false, bytesRead, errDeadline
return bytesRead, errDeadline
}
if deadlineTimer == nil {
deadlineTimer = utils.NewTimer()
@ -161,10 +184,10 @@ func (s *receiveStream) readImpl(p []byte) (bool /*stream completed */, int, err
}
if bytesRead > len(p) {
return false, bytesRead, fmt.Errorf("BUG: bytesRead (%d) > len(p) (%d) in stream.Read", bytesRead, len(p))
return bytesRead, fmt.Errorf("BUG: bytesRead (%d) > len(p) (%d) in stream.Read", bytesRead, len(p))
}
if s.readPosInFrame > len(s.currentFrame) {
return false, bytesRead, fmt.Errorf("BUG: readPosInFrame (%d) > frame.DataLen (%d) in stream.Read", s.readPosInFrame, len(s.currentFrame))
return bytesRead, fmt.Errorf("BUG: readPosInFrame (%d) > frame.DataLen (%d) in stream.Read", s.readPosInFrame, len(s.currentFrame))
}
m := copy(p[bytesRead:], s.currentFrame[s.readPosInFrame:])
@ -173,20 +196,20 @@ func (s *receiveStream) readImpl(p []byte) (bool /*stream completed */, int, err
// when a RESET_STREAM was received, the flow controller was already
// informed about the final byteOffset for this stream
if s.resetRemotelyErr == nil {
if !s.cancelledRemotely {
s.flowController.AddBytesRead(protocol.ByteCount(m))
}
if s.readPosInFrame >= len(s.currentFrame) && s.currentFrameIsLast {
s.finRead = true
s.currentFrame = nil
if s.currentFrameDone != nil {
s.currentFrameDone()
}
return true, bytesRead, io.EOF
s.errorRead = true
return bytesRead, io.EOF
}
}
return false, bytesRead, nil
return bytesRead, nil
}
func (s *receiveStream) dequeueNextFrame() {
@ -202,7 +225,8 @@ func (s *receiveStream) dequeueNextFrame() {
func (s *receiveStream) CancelRead(errorCode StreamErrorCode) {
s.mutex.Lock()
completed := s.cancelReadImpl(errorCode)
s.cancelReadImpl(errorCode)
completed := s.isNewlyCompleted()
s.mutex.Unlock()
if completed {
@ -211,23 +235,26 @@ func (s *receiveStream) CancelRead(errorCode StreamErrorCode) {
}
}
func (s *receiveStream) cancelReadImpl(errorCode qerr.StreamErrorCode) bool /* completed */ {
if s.finRead || s.cancelReadErr != nil || s.resetRemotelyErr != nil {
return false
func (s *receiveStream) cancelReadImpl(errorCode qerr.StreamErrorCode) {
if s.cancelledLocally { // duplicate call to CancelRead
return
}
s.cancelReadErr = &StreamError{StreamID: s.streamID, ErrorCode: errorCode, Remote: false}
s.cancelledLocally = true
if s.errorRead || s.cancelledRemotely {
return
}
s.cancelErr = &StreamError{StreamID: s.streamID, ErrorCode: errorCode, Remote: false}
s.signalRead()
s.sender.queueControlFrame(&wire.StopSendingFrame{
StreamID: s.streamID,
ErrorCode: errorCode,
})
// We're done with this stream if the final offset was already received.
return s.finalOffset != protocol.MaxByteCount
}
func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame) error {
s.mutex.Lock()
completed, err := s.handleStreamFrameImpl(frame)
err := s.handleStreamFrameImpl(frame)
completed := s.isNewlyCompleted()
s.mutex.Unlock()
if completed {
@ -237,59 +264,58 @@ func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame) error {
return err
}
func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame) (bool /* completed */, error) {
func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame) error {
maxOffset := frame.Offset + frame.DataLen()
if err := s.flowController.UpdateHighestReceived(maxOffset, frame.Fin); err != nil {
return false, err
return err
}
var newlyRcvdFinalOffset bool
if frame.Fin {
newlyRcvdFinalOffset = s.finalOffset == protocol.MaxByteCount
s.finalOffset = maxOffset
}
if s.cancelReadErr != nil {
return newlyRcvdFinalOffset, nil
if s.cancelledLocally {
return nil
}
if err := s.frameQueue.Push(frame.Data, frame.Offset, frame.PutBack); err != nil {
return false, err
return err
}
s.signalRead()
return false, nil
return nil
}
func (s *receiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame) error {
s.mutex.Lock()
completed, err := s.handleResetStreamFrameImpl(frame)
err := s.handleResetStreamFrameImpl(frame)
completed := s.isNewlyCompleted()
s.mutex.Unlock()
if completed {
s.flowController.Abandon()
s.sender.onStreamCompleted(s.streamID)
}
return err
}
func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame) (bool /*completed */, error) {
func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame) error {
if s.closeForShutdownErr != nil {
return false, nil
return nil
}
if err := s.flowController.UpdateHighestReceived(frame.FinalSize, true); err != nil {
return false, err
return err
}
newlyRcvdFinalOffset := s.finalOffset == protocol.MaxByteCount
s.finalOffset = frame.FinalSize
// ignore duplicate RESET_STREAM frames for this stream (after checking their final offset)
if s.resetRemotelyErr != nil {
return false, nil
if s.cancelledRemotely {
return nil
}
s.resetRemotelyErr = &StreamError{
StreamID: s.streamID,
ErrorCode: frame.ErrorCode,
Remote: true,
s.flowController.Abandon()
// don't save the error if the RESET_STREAM frames was received after CancelRead was called
if s.cancelledLocally {
return nil
}
s.cancelledRemotely = true
s.cancelErr = &StreamError{StreamID: s.streamID, ErrorCode: frame.ErrorCode, Remote: true}
s.signalRead()
return newlyRcvdFinalOffset, nil
return nil
}
func (s *receiveStream) SetReadDeadline(t time.Time) error {

View File

@ -42,7 +42,11 @@ type sendStream struct {
finishedWriting bool // set once Close() is called
finSent bool // set when a STREAM_FRAME with FIN bit has been sent
completed bool // set when this stream has been reported to the streamSender as completed
// Set when the application knows about the cancellation.
// This can happen because the application called CancelWrite,
// or because Write returned the error (for remote cancellations).
cancellationFlagged bool
completed bool // set when this stream has been reported to the streamSender as completed
dataForWriting []byte // during a Write() call, this slice is the part of p that still needs to be sent out
nextFrame *wire.StreamFrame
@ -60,6 +64,7 @@ type sendStream struct {
)
func newSendStream(
ctx context.Context,
streamID protocol.StreamID,
sender streamSender,
flowController flowcontrol.StreamFlowController,
@ -71,7 +76,7 @@ func newSendStream(
writeChan: make(chan struct{}, 1),
writeOnce: make(chan struct{}, 1), // cap: 1, to protect against concurrent use of Write
}
s.ctx, s.ctxCancel = context.WithCancelCause(context.Background())
s.ctx, s.ctxCancel = context.WithCancelCause(ctx)
return s
}
@ -86,23 +91,32 @@ func (s *sendStream) Write(p []byte) (int, error) {
s.writeOnce <- struct{}{}
defer func() { <-s.writeOnce }()
isNewlyCompleted, n, err := s.write(p)
if isNewlyCompleted {
s.sender.onStreamCompleted(s.streamID)
}
return n, err
}
func (s *sendStream) write(p []byte) (bool /* is newly completed */, int, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.finishedWriting {
return 0, fmt.Errorf("write on closed stream %d", s.streamID)
return false, 0, fmt.Errorf("write on closed stream %d", s.streamID)
}
if s.cancelWriteErr != nil {
return 0, s.cancelWriteErr
s.cancellationFlagged = true
return s.isNewlyCompleted(), 0, s.cancelWriteErr
}
if s.closeForShutdownErr != nil {
return 0, s.closeForShutdownErr
return false, 0, s.closeForShutdownErr
}
if !s.deadline.IsZero() && !time.Now().Before(s.deadline) {
return 0, errDeadline
return false, 0, errDeadline
}
if len(p) == 0 {
return 0, nil
return false, 0, nil
}
s.dataForWriting = p
@ -143,7 +157,7 @@ func (s *sendStream) Write(p []byte) (int, error) {
if !deadline.IsZero() {
if !time.Now().Before(deadline) {
s.dataForWriting = nil
return bytesWritten, errDeadline
return false, bytesWritten, errDeadline
}
if deadlineTimer == nil {
deadlineTimer = utils.NewTimer()
@ -178,14 +192,15 @@ func (s *sendStream) Write(p []byte) (int, error) {
}
if bytesWritten == len(p) {
return bytesWritten, nil
return false, bytesWritten, nil
}
if s.closeForShutdownErr != nil {
return bytesWritten, s.closeForShutdownErr
return false, bytesWritten, s.closeForShutdownErr
} else if s.cancelWriteErr != nil {
return bytesWritten, s.cancelWriteErr
s.cancellationFlagged = true
return s.isNewlyCompleted(), bytesWritten, s.cancelWriteErr
}
return bytesWritten, nil
return false, bytesWritten, nil
}
func (s *sendStream) canBufferStreamFrame() bool {
@ -348,8 +363,24 @@ func (s *sendStream) getDataForWriting(f *wire.StreamFrame, maxBytes protocol.By
}
func (s *sendStream) isNewlyCompleted() bool {
completed := (s.finSent || s.cancelWriteErr != nil) && s.numOutstandingFrames == 0 && len(s.retransmissionQueue) == 0
if completed && !s.completed {
if s.completed {
return false
}
// We need to keep the stream around until all frames have been sent and acknowledged.
if s.numOutstandingFrames > 0 || len(s.retransmissionQueue) > 0 {
return false
}
// The stream is completed if we sent the FIN.
if s.finSent {
s.completed = true
return true
}
// The stream is also completed if:
// 1. the application called CancelWrite, or
// 2. we received a STOP_SENDING, and
// * the application consumed the error via Write, or
// * the application called CLsoe
if s.cancelWriteErr != nil && (s.cancellationFlagged || s.finishedWriting) {
s.completed = true
return true
}
@ -362,15 +393,23 @@ func (s *sendStream) Close() error {
s.mutex.Unlock()
return nil
}
if s.cancelWriteErr != nil {
s.mutex.Unlock()
return fmt.Errorf("close called for canceled stream %d", s.streamID)
}
s.ctxCancel(nil)
s.finishedWriting = true
cancelWriteErr := s.cancelWriteErr
if cancelWriteErr != nil {
s.cancellationFlagged = true
}
completed := s.isNewlyCompleted()
s.mutex.Unlock()
if completed {
s.sender.onStreamCompleted(s.streamID)
}
if cancelWriteErr != nil {
return fmt.Errorf("close called for canceled stream %d", s.streamID)
}
s.sender.onHasStreamData(s.streamID) // need to send the FIN, must be called without holding the mutex
s.ctxCancel(nil)
return nil
}
@ -378,9 +417,11 @@ func (s *sendStream) CancelWrite(errorCode StreamErrorCode) {
s.cancelWriteImpl(errorCode, false)
}
// must be called after locking the mutex
func (s *sendStream) cancelWriteImpl(errorCode qerr.StreamErrorCode, remote bool) {
s.mutex.Lock()
if !remote {
s.cancellationFlagged = true
}
if s.cancelWriteErr != nil {
s.mutex.Unlock()
return

View File

@ -92,7 +92,7 @@ type baseServer struct {
*handshake.TokenGenerator,
bool, /* client address validated by an address validation token */
*logging.ConnectionTracer,
uint64,
ConnectionTracingID,
utils.Logger,
protocol.Version,
) quicConn

View File

@ -1,6 +1,7 @@
package quic
import (
"context"
"net"
"os"
"sync"
@ -85,7 +86,9 @@ type stream struct {
var _ Stream = &stream{}
// newStream creates a new Stream
func newStream(streamID protocol.StreamID,
func newStream(
ctx context.Context,
streamID protocol.StreamID,
sender streamSender,
flowController flowcontrol.StreamFlowController,
) *stream {
@ -99,7 +102,7 @@ func newStream(streamID protocol.StreamID,
s.completedMutex.Unlock()
},
}
s.sendStream = *newSendStream(streamID, senderForSendStream, flowController)
s.sendStream = *newSendStream(ctx, streamID, senderForSendStream, flowController)
senderForReceiveStream := &uniStreamSender{
streamSender: sender,
onStreamCompletedImpl: func() {

View File

@ -45,6 +45,7 @@ func (streamOpenErr) Timeout() bool { return false }
var errTooManyOpenStreams = errors.New("too many open streams")
type streamsMap struct {
ctx context.Context // not used for cancellations, but carries the values associated with the connection
perspective protocol.Perspective
maxIncomingBidiStreams uint64
@ -64,6 +65,7 @@ type streamsMap struct {
var _ streamManager = &streamsMap{}
func newStreamsMap(
ctx context.Context,
sender streamSender,
newFlowController func(protocol.StreamID) flowcontrol.StreamFlowController,
maxIncomingBidiStreams uint64,
@ -71,6 +73,7 @@ func newStreamsMap(
perspective protocol.Perspective,
) streamManager {
m := &streamsMap{
ctx: ctx,
perspective: perspective,
newFlowController: newFlowController,
maxIncomingBidiStreams: maxIncomingBidiStreams,
@ -86,7 +89,7 @@ func (m *streamsMap) initMaps() {
protocol.StreamTypeBidi,
func(num protocol.StreamNum) streamI {
id := num.StreamID(protocol.StreamTypeBidi, m.perspective)
return newStream(id, m.sender, m.newFlowController(id))
return newStream(m.ctx, id, m.sender, m.newFlowController(id))
},
m.sender.queueControlFrame,
)
@ -94,7 +97,7 @@ func(num protocol.StreamNum) streamI {
protocol.StreamTypeBidi,
func(num protocol.StreamNum) streamI {
id := num.StreamID(protocol.StreamTypeBidi, m.perspective.Opposite())
return newStream(id, m.sender, m.newFlowController(id))
return newStream(m.ctx, id, m.sender, m.newFlowController(id))
},
m.maxIncomingBidiStreams,
m.sender.queueControlFrame,
@ -103,7 +106,7 @@ func(num protocol.StreamNum) streamI {
protocol.StreamTypeUni,
func(num protocol.StreamNum) sendStreamI {
id := num.StreamID(protocol.StreamTypeUni, m.perspective)
return newSendStream(id, m.sender, m.newFlowController(id))
return newSendStream(m.ctx, id, m.sender, m.newFlowController(id))
},
m.sender.queueControlFrame,
)

View File

@ -33,4 +33,6 @@ func parseIPv4PktInfo(body []byte) (ip netip.Addr, ifIndex uint32, ok bool) {
return netip.AddrFrom4(*(*[4]byte)(body[8:12])), binary.LittleEndian.Uint32(body), true
}
func isGSOSupported(syscall.RawConn) bool { return false }
func isGSOEnabled(syscall.RawConn) bool { return false }
func isECNEnabled() bool { return !isECNDisabledUsingEnv() }

View File

@ -28,4 +28,6 @@ func parseIPv4PktInfo(body []byte) (ip netip.Addr, _ uint32, ok bool) {
return netip.AddrFrom4(*(*[4]byte)(body)), 0, true
}
func isGSOSupported(syscall.RawConn) bool { return false }
func isGSOEnabled(syscall.RawConn) bool { return false }
func isECNEnabled() bool { return !isECNDisabledUsingEnv() }

View File

@ -23,6 +23,12 @@
const batchSize = 8 // needs to smaller than MaxUint8 (otherwise the type of oobConn.readPos has to be changed)
var kernelVersionMajor int
func init() {
kernelVersionMajor, _ = kernelVersion()
}
func forceSetReceiveBuffer(c syscall.RawConn, bytes int) error {
var serr error
if err := c.Control(func(fd uintptr) {
@ -55,9 +61,12 @@ func parseIPv4PktInfo(body []byte) (ip netip.Addr, ifIndex uint32, ok bool) {
return netip.AddrFrom4(*(*[4]byte)(body[8:12])), binary.LittleEndian.Uint32(body), true
}
// isGSOSupported tests if the kernel supports GSO.
// isGSOEnabled tests if the kernel supports GSO.
// Sending with GSO might still fail later on, if the interface doesn't support it (see isGSOError).
func isGSOSupported(conn syscall.RawConn) bool {
func isGSOEnabled(conn syscall.RawConn) bool {
if kernelVersionMajor < 5 {
return false
}
disabled, err := strconv.ParseBool(os.Getenv("QUIC_GO_DISABLE_GSO"))
if err == nil && disabled {
return false
@ -108,3 +117,40 @@ func isPermissionError(err error) bool {
}
return false
}
func isECNEnabled() bool {
return kernelVersionMajor >= 5 && !isECNDisabledUsingEnv()
}
// kernelVersion returns major and minor kernel version numbers, parsed from
// the syscall.Uname's Release field, or 0, 0 if the version can't be obtained
// or parsed.
//
// copied from the standard library's internal/syscall/unix/kernel_version_linux.go
func kernelVersion() (major, minor int) {
var uname syscall.Utsname
if err := syscall.Uname(&uname); err != nil {
return
}
var (
values [2]int
value, vi int
)
for _, c := range uname.Release {
if '0' <= c && c <= '9' {
value = (value * 10) + int(c-'0')
} else {
// Note that we're assuming N.N.N here.
// If we see anything else, we are likely to mis-parse it.
values[vi] = value
vi++
if vi >= len(values) {
break
}
value = 0
}
}
return values[0], values[1]
}

View File

@ -59,7 +59,7 @@ func inspectWriteBuffer(c syscall.RawConn) (int, error) {
return size, serr
}
func isECNDisabled() bool {
func isECNDisabledUsingEnv() bool {
disabled, err := strconv.ParseBool(os.Getenv("QUIC_GO_DISABLE_ECN"))
return err == nil && disabled
}
@ -147,8 +147,8 @@ func newConn(c OOBCapablePacketConn, supportsDF bool) (*oobConn, error) {
readPos: batchSize,
cap: connCapabilities{
DF: supportsDF,
GSO: isGSOSupported(rawConn),
ECN: !isECNDisabled(),
GSO: isGSOEnabled(rawConn),
ECN: isECNEnabled(),
},
}
for i := 0; i < batchSize; i++ {
@ -247,7 +247,7 @@ func (c *oobConn) WritePacket(b []byte, addr net.Addr, packetInfoOOB []byte, gso
}
if ecn != protocol.ECNUnsupported {
if !c.capabilities().ECN {
panic("tried to send a ECN-marked packet although ECN is disabled")
panic("tried to send an ECN-marked packet although ECN is disabled")
}
if remoteUDPAddr, ok := addr.(*net.UDPAddr); ok {
if remoteUDPAddr.IP.To4() != nil {

2
vendor/modules.txt vendored
View File

@ -104,7 +104,7 @@ github.com/powerman/deepequal
# github.com/quic-go/qpack v0.4.0
## explicit; go 1.18
github.com/quic-go/qpack
# github.com/quic-go/quic-go v0.42.0
# github.com/quic-go/quic-go v0.43.0
## explicit; go 1.21
github.com/quic-go/quic-go
github.com/quic-go/quic-go/http3