Skip to content

Commit 3e3ca88

Browse files
committed
port quic-go v0.52.0
Signed-off-by: roc <roc@imroc.cc>
1 parent fa3e965 commit 3e3ca88

7 files changed

Lines changed: 192 additions & 100 deletions

File tree

internal/http3/body.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/quic-go/quic-go"
1010
)
1111

12-
// A Hijacker allows hijacking of the stream creating part of a quic.Session from a http.Response.Body.
12+
// A Hijacker allows hijacking of the stream creating part of a quic.Connection from a http.ResponseWriter.
1313
// It is used by WebTransport to create WebTransport streams after a session has been established.
1414
type Hijacker interface {
1515
Connection() Connection

internal/http3/client.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ const (
3232
defaultMaxResponseHeaderBytes = 10 * 1 << 20 // 10 MB
3333
)
3434

35+
type errConnUnusable struct{ e error }
36+
37+
func (e *errConnUnusable) Unwrap() error { return e.e }
38+
func (e *errConnUnusable) Error() string { return fmt.Sprintf("http3: conn unusable: %s", e.e.Error()) }
39+
40+
const max1xxResponses = 5 // arbitrary bound on number of informational responses
41+
3542
var defaultQuicConfig = &quic.Config{
3643
MaxIncomingStreams: -1, // don't allow the server to create bidirectional streams
3744
KeepAlivePeriod: 10 * time.Second,
@@ -233,7 +240,7 @@ func (c *ClientConn) roundTrip(req *http.Request) (*http.Response, error) {
233240
c.maxResponseHeaderBytes,
234241
)
235242
if err != nil {
236-
return nil, err
243+
return nil, &errConnUnusable{e: err}
237244
}
238245

239246
// Request Cancellation:
@@ -346,8 +353,7 @@ func (c *ClientConn) doRequest(req *http.Request, str *requestStream) (*http.Res
346353
}
347354

348355
// copy from net/http: support 1xx responses
349-
num1xx := 0 // number of informational 1xx headers received
350-
const max1xxResponses = 5 // arbitrary bound on number of informational responses
356+
var num1xx int // number of informational 1xx headers received
351357

352358
var res *http.Response
353359
for {
@@ -363,10 +369,12 @@ func (c *ClientConn) doRequest(req *http.Request, str *requestStream) (*http.Res
363369
if is1xxNonTerminal {
364370
num1xx++
365371
if num1xx > max1xxResponses {
366-
return nil, errors.New("http: too many 1xx informational responses")
372+
str.CancelRead(quic.StreamErrorCode(ErrCodeExcessiveLoad))
373+
str.CancelWrite(quic.StreamErrorCode(ErrCodeExcessiveLoad))
374+
return nil, errors.New("http3: too many 1xx informational responses")
367375
}
368376
traceGot1xxResponse(trace, resCode, textproto.MIMEHeader(res.Header))
369-
if resCode == 100 {
377+
if resCode == http.StatusContinue {
370378
traceGot100Continue(trace)
371379
}
372380
continue

internal/http3/conn.go

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package http3
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"log/slog"
@@ -19,6 +20,8 @@ import (
1920
"github.com/quic-go/qpack"
2021
)
2122

23+
var errGoAway = errors.New("connection in graceful shutdown")
24+
2225
// Connection is an HTTP/3 connection.
2326
// It has all methods from the quic.Connection expect for AcceptStream, AcceptUniStream,
2427
// SendDatagram and ReceiveDatagram.
@@ -51,8 +54,10 @@ type connection struct {
5154

5255
decoder *qpack.Decoder
5356

54-
streamMx sync.Mutex
55-
streams map[quic.StreamID]*datagrammer
57+
streamMx sync.Mutex
58+
streams map[quic.StreamID]*datagrammer
59+
lastStreamID quic.StreamID
60+
maxStreamID quic.StreamID
5661

5762
settings *Settings
5863
receivedSettings chan struct{}
@@ -80,6 +85,8 @@ func newConnection(
8085
decoder: qpack.NewDecoder(func(hf qpack.HeaderField) {}),
8186
receivedSettings: make(chan struct{}),
8287
streams: make(map[quic.StreamID]*datagrammer),
88+
maxStreamID: InvalidStreamID,
89+
lastStreamID: InvalidStreamID,
8390
Options: options,
8491
}
8592
if idleTimeout > 0 {
@@ -100,6 +107,13 @@ func (c *connection) clearStream(id quic.StreamID) {
100107
if c.idleTimeout > 0 && len(c.streams) == 0 {
101108
c.idleTimer.Reset(c.idleTimeout)
102109
}
110+
// The server is performing a graceful shutdown.
111+
// If no more streams are remaining, close the connection.
112+
if c.maxStreamID != InvalidStreamID {
113+
if len(c.streams) == 0 {
114+
c.CloseWithError(quic.ApplicationErrorCode(ErrCodeNoError), "")
115+
}
116+
}
103117
}
104118

105119
func (c *connection) openRequestStream(
@@ -109,13 +123,30 @@ func (c *connection) openRequestStream(
109123
disableCompression bool,
110124
maxHeaderBytes uint64,
111125
) (*requestStream, error) {
126+
if c.perspective == PerspectiveClient {
127+
c.streamMx.Lock()
128+
maxStreamID := c.maxStreamID
129+
var nextStreamID quic.StreamID
130+
if c.lastStreamID == InvalidStreamID {
131+
nextStreamID = 0
132+
} else {
133+
nextStreamID = c.lastStreamID + 4
134+
}
135+
c.streamMx.Unlock()
136+
// Streams with stream ID equal to or greater than the stream ID carried in the GOAWAY frame
137+
// will be rejected, see section 5.2 of RFC 9114.
138+
if maxStreamID != InvalidStreamID && nextStreamID >= maxStreamID {
139+
return nil, errGoAway
140+
}
141+
}
112142
str, err := c.OpenStreamSync(ctx)
113143
if err != nil {
114144
return nil, err
115145
}
116146
datagrams := newDatagrammer(func(b []byte) error { return c.sendDatagram(str.StreamID(), b) })
117147
c.streamMx.Lock()
118148
c.streams[str.StreamID()] = datagrams
149+
c.lastStreamID = str.StreamID()
119150
c.streamMx.Unlock()
120151
qstr := newStateTrackingStream(str, c, datagrams)
121152
rsp := &http.Response{}
@@ -247,41 +278,94 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
247278
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeStreamCreationError), "duplicate control stream")
248279
return
249280
}
250-
fp := &frameParser{conn: c.Connection, r: str}
251-
f, err := fp.ParseNext()
252-
if err != nil {
253-
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
254-
return
255-
}
256-
sf, ok := f.(*settingsFrame)
257-
if !ok {
258-
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
259-
return
260-
}
261-
c.settings = &Settings{
262-
EnableDatagrams: sf.Datagram,
263-
EnableExtendedConnect: sf.ExtendedConnect,
264-
Other: sf.Other,
265-
}
266-
close(c.receivedSettings)
267-
if !sf.Datagram {
268-
return
281+
c.handleControlStream(str)
282+
}(str)
283+
}
284+
}
285+
286+
func (c *connection) handleControlStream(str quic.ReceiveStream) {
287+
fp := &frameParser{conn: c.Connection, r: str}
288+
f, err := fp.ParseNext()
289+
if err != nil {
290+
var serr *quic.StreamError
291+
if err == io.EOF || errors.As(err, &serr) {
292+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeClosedCriticalStream), "")
293+
return
294+
}
295+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
296+
return
297+
}
298+
sf, ok := f.(*settingsFrame)
299+
if !ok {
300+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeMissingSettings), "")
301+
return
302+
}
303+
c.settings = &Settings{
304+
EnableDatagrams: sf.Datagram,
305+
EnableExtendedConnect: sf.ExtendedConnect,
306+
Other: sf.Other,
307+
}
308+
close(c.receivedSettings)
309+
if sf.Datagram {
310+
// If datagram support was enabled on our side as well as on the server side,
311+
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
312+
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
313+
if c.enableDatagrams && !c.ConnectionState().SupportsDatagrams {
314+
c.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
315+
return
316+
}
317+
go func() {
318+
if err := c.receiveDatagrams(); err != nil {
319+
if c.logger != nil {
320+
c.logger.Debug("receiving datagrams failed", "error", err)
321+
}
269322
}
270-
// If datagram support was enabled on our side as well as on the server side,
271-
// we can expect it to have been negotiated both on the transport and on the HTTP/3 layer.
272-
// Note: ConnectionState() will block until the handshake is complete (relevant when using 0-RTT).
273-
if c.enableDatagrams && !c.ConnectionState().SupportsDatagrams {
274-
c.CloseWithError(quic.ApplicationErrorCode(ErrCodeSettingsError), "missing QUIC Datagram support")
323+
}()
324+
}
325+
326+
// we don't support server push, hence we don't expect any GOAWAY frames from the client
327+
if c.perspective == PerspectiveServer {
328+
return
329+
}
330+
331+
for {
332+
f, err := fp.ParseNext()
333+
if err != nil {
334+
var serr *quic.StreamError
335+
if err == io.EOF || errors.As(err, &serr) {
336+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeClosedCriticalStream), "")
275337
return
276338
}
277-
go func() {
278-
if err := c.receiveDatagrams(); err != nil {
279-
if c.logger != nil {
280-
c.logger.Debug("receiving datagrams failed", "error", err)
281-
}
282-
}
283-
}()
284-
}(str)
339+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameError), "")
340+
return
341+
}
342+
// GOAWAY is the only frame allowed at this point:
343+
// * unexpected frames are ignored by the frame parser
344+
// * we don't support any extension that might add support for more frames
345+
goaway, ok := f.(*goAwayFrame)
346+
if !ok {
347+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "")
348+
return
349+
}
350+
if goaway.StreamID%4 != 0 { // client-initiated, bidirectional streams
351+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
352+
return
353+
}
354+
c.streamMx.Lock()
355+
if c.maxStreamID != InvalidStreamID && goaway.StreamID > c.maxStreamID {
356+
c.streamMx.Unlock()
357+
c.Connection.CloseWithError(quic.ApplicationErrorCode(ErrCodeIDError), "")
358+
return
359+
}
360+
c.maxStreamID = goaway.StreamID
361+
hasActiveStreams := len(c.streams) > 0
362+
c.streamMx.Unlock()
363+
364+
// immediately close the connection if there are currently no active requests
365+
if !hasActiveStreams {
366+
c.CloseWithError(quic.ApplicationErrorCode(ErrCodeNoError), "")
367+
return
368+
}
285369
}
286370
}
287371

@@ -311,11 +395,10 @@ func (c *connection) receiveDatagrams() error {
311395
streamID := quic.StreamID(4 * quarterStreamID)
312396
c.streamMx.Lock()
313397
dg, ok := c.streams[streamID]
398+
c.streamMx.Unlock()
314399
if !ok {
315-
c.streamMx.Unlock()
316-
return nil
400+
continue
317401
}
318-
c.streamMx.Unlock()
319402
dg.enqueue(b[n:])
320403
}
321404
}

internal/http3/headers.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,6 @@ func requestFromHeaders(headerFields []qpack.HeaderField) (*http.Request, error)
211211
}, nil
212212
}
213213

214-
func hostnameFromURL(url *url.URL) string {
215-
if url != nil {
216-
return url.Host
217-
}
218-
return ""
219-
}
220-
221214
// updateResponseFromHeaders sets up http.Response as an HTTP/3 response,
222215
// using the decoded qpack header filed.
223216
// It is only called for the HTTP header (and not the HTTP trailer).
@@ -228,7 +221,7 @@ func updateResponseFromHeaders(rsp *http.Response, headerFields []qpack.HeaderFi
228221
return err
229222
}
230223
if hdr.Status == "" {
231-
return errors.New("missing status field")
224+
return errors.New("missing :status field")
232225
}
233226
rsp.Proto = "HTTP/3.0"
234227
rsp.ProtoMajor = 3

internal/http3/protocol.go

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,9 @@ const (
5353
StreamTypeBidi
5454
)
5555

56-
// A StreamID in QUIC
57-
type StreamID int64
58-
59-
// InitiatedBy says if the stream was initiated by the client or by the server
60-
func (s StreamID) InitiatedBy() Perspective {
61-
if s%2 == 0 {
62-
return PerspectiveClient
63-
}
64-
return PerspectiveServer
65-
}
66-
67-
// Type says if this is a unidirectional or bidirectional stream
68-
func (s StreamID) Type() StreamType {
69-
if s%4 >= 2 {
70-
return StreamTypeUni
71-
}
72-
return StreamTypeBidi
73-
}
74-
75-
// StreamNum returns how many streams in total are below this
76-
// Example: for stream 9 it returns 3 (i.e. streams 1, 5 and 9)
77-
func (s StreamID) StreamNum() StreamNum {
78-
return StreamNum(s/4) + 1
79-
}
80-
8156
// InvalidPacketNumber is a stream ID that is invalid.
8257
// The first valid stream ID in QUIC is 0.
83-
const InvalidStreamID StreamID = -1
58+
const InvalidStreamID quic.StreamID = -1
8459

8560
// StreamNum is the stream number
8661
type StreamNum int64
@@ -94,11 +69,11 @@ const (
9469
)
9570

9671
// StreamID calculates the stream ID.
97-
func (s StreamNum) StreamID(stype StreamType, pers Perspective) StreamID {
72+
func (s StreamNum) StreamID(stype StreamType, pers Perspective) quic.StreamID {
9873
if s == 0 {
9974
return InvalidStreamID
10075
}
101-
var first StreamID
76+
var first quic.StreamID
10277
switch stype {
10378
case StreamTypeBidi:
10479
switch pers {
@@ -115,5 +90,5 @@ func (s StreamNum) StreamID(stype StreamType, pers Perspective) StreamID {
11590
first = 3
11691
}
11792
}
118-
return first + 4*StreamID(s-1)
93+
return first + 4*quic.StreamID(s-1)
11994
}

internal/http3/request_writer.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
reqheader "github.com/imroc/req/v3/internal/header"
1717
"github.com/quic-go/qpack"
1818

19-
"github.com/quic-go/quic-go"
2019
"golang.org/x/net/http/httpguts"
2120
"golang.org/x/net/http2/hpack"
2221
"golang.org/x/net/idna"
@@ -39,13 +38,13 @@ func newRequestWriter() *requestWriter {
3938
}
4039
}
4140

42-
func (w *requestWriter) WriteRequestHeader(str quic.Stream, req *http.Request, gzip bool, dumps []*dump.Dumper) error {
41+
func (w *requestWriter) WriteRequestHeader(wr io.Writer, req *http.Request, gzip bool, dumps []*dump.Dumper) error {
4342
// TODO: figure out how to add support for trailers
4443
buf := &bytes.Buffer{}
4544
if err := w.writeHeaders(buf, req, gzip, dumps); err != nil {
4645
return err
4746
}
48-
if _, err := str.Write(buf.Bytes()); err != nil {
47+
if _, err := wr.Write(buf.Bytes()); err != nil {
4948
return err
5049
}
5150
trace := httptrace.ContextClientTrace(req.Context())

0 commit comments

Comments
 (0)