| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build go1.24 |
| |
| package http3 |
| |
| import ( |
| "context" |
| "crypto/tls" |
| "errors" |
| "fmt" |
| "io" |
| "net/http" |
| "sync" |
| |
| "golang.org/x/net/quic" |
| ) |
| |
| // A Transport is an HTTP/3 transport. |
| // |
| // It does not manage a pool of connections, |
| // and therefore does not implement net/http.RoundTripper. |
| // |
| // TODO: Provide a way to register an HTTP/3 transport with a net/http.Transport's |
| // connection pool. |
| type Transport struct { |
| // Endpoint is the QUIC endpoint used by connections created by the transport. |
| // If unset, it is initialized by the first call to Dial. |
| Endpoint *quic.Endpoint |
| |
| // Config is the QUIC configuration used for client connections. |
| // The Config may be nil. |
| // |
| // Dial may clone and modify the Config. |
| // The Config must not be modified after calling Dial. |
| Config *quic.Config |
| |
| initOnce sync.Once |
| initErr error |
| } |
| |
| func (tr *Transport) init() error { |
| tr.initOnce.Do(func() { |
| if tr.Config == nil { |
| tr.Config = &quic.Config{} |
| } |
| |
| // maybeCloneTLSConfig clones the user-provided tls.Config (but only once) |
| // prior to us modifying it. |
| needCloneTLSConfig := true |
| maybeCloneTLSConfig := func() *tls.Config { |
| if needCloneTLSConfig { |
| tr.Config.TLSConfig = tr.Config.TLSConfig.Clone() |
| needCloneTLSConfig = false |
| } |
| return tr.Config.TLSConfig |
| } |
| |
| if tr.Config.TLSConfig == nil { |
| tr.Config.TLSConfig = &tls.Config{} |
| needCloneTLSConfig = false |
| } |
| if tr.Config.TLSConfig.MinVersion == 0 { |
| maybeCloneTLSConfig().MinVersion = tls.VersionTLS13 |
| } |
| if tr.Config.TLSConfig.NextProtos == nil { |
| maybeCloneTLSConfig().NextProtos = []string{"h3"} |
| } |
| if tr.Endpoint == nil { |
| tr.Endpoint, tr.initErr = quic.Listen("udp", ":0", nil) |
| } |
| }) |
| return tr.initErr |
| } |
| |
| // Dial creates a new HTTP/3 client connection. |
| func (tr *Transport) Dial(ctx context.Context, target string) (*ClientConn, error) { |
| if err := tr.init(); err != nil { |
| return nil, err |
| } |
| qconn, err := tr.Endpoint.Dial(ctx, "udp", target, tr.Config) |
| if err != nil { |
| return nil, err |
| } |
| return newClientConn(ctx, qconn) |
| } |
| |
| // A ClientConn is a client HTTP/3 connection. |
| // |
| // Multiple goroutines may invoke methods on a ClientConn simultaneously. |
| type ClientConn struct { |
| qconn *quic.Conn |
| |
| mu sync.Mutex |
| |
| // The peer may create exactly one control, encoder, and decoder stream. |
| // streamsCreated is a bitset of streams created so far. |
| // Bits are 1 << streamType. |
| streamsCreated uint8 |
| } |
| |
| func newClientConn(ctx context.Context, qconn *quic.Conn) (*ClientConn, error) { |
| cc := &ClientConn{ |
| qconn: qconn, |
| } |
| |
| // Create control stream and send SETTINGS frame. |
| controlStream, err := newConnStream(ctx, cc.qconn, streamTypeControl) |
| if err != nil { |
| return nil, fmt.Errorf("http3: cannot create control stream: %v", err) |
| } |
| controlStream.writeSettings() |
| controlStream.Flush() |
| |
| go cc.acceptStreams() |
| return cc, nil |
| } |
| |
| // Close closes the connection. |
| // Any in-flight requests are canceled. |
| // Close does not wait for the peer to acknowledge the connection closing. |
| func (cc *ClientConn) Close() error { |
| // Close the QUIC connection immediately with a status of NO_ERROR. |
| cc.qconn.Abort(nil) |
| |
| // Return any existing error from the peer, but don't wait for it. |
| ctx, cancel := context.WithCancel(context.Background()) |
| cancel() |
| return cc.qconn.Wait(ctx) |
| } |
| |
| // RoundTrip sends a request on the connection. |
| func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { |
| return nil, errors.New("TODO") |
| } |
| |
| func (cc *ClientConn) acceptStreams() { |
| for { |
| // Use context.Background: This blocks until a stream is accepted |
| // or the connection closes. |
| st, err := cc.qconn.AcceptStream(context.Background()) |
| if err != nil { |
| return // connection closed |
| } |
| if !st.IsReadOnly() { |
| // "Clients MUST treat receipt of a server-initiated bidirectional |
| // stream as a connection error of type H3_STREAM_CREATION_ERROR [...]" |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.1-3 |
| cc.qconn.Abort(&quic.ApplicationError{ |
| Code: uint64(errH3StreamCreationError), |
| Reason: "server created bidirectional stream", |
| }) |
| return |
| } |
| go cc.handleStream(newStream(st)) |
| } |
| } |
| |
| func (cc *ClientConn) handleStream(st *stream) { |
| // Unidirectional stream header: One varint with the stream type. |
| stype, err := st.readVarint() |
| if err != nil { |
| cc.qconn.Abort(&quic.ApplicationError{ |
| Code: uint64(errH3StreamCreationError), |
| Reason: "error reading unidirectional stream header", |
| }) |
| return |
| } |
| switch streamType(stype) { |
| case streamTypeControl: |
| err = cc.handleControlStream(st) |
| case streamTypePush: |
| err = cc.handlePushStream(st) |
| case streamTypeEncoder: |
| err = cc.handleEncoderStream(st) |
| case streamTypeDecoder: |
| err = cc.handleDecoderStream(st) |
| default: |
| // "Recipients of unknown stream types MUST either abort reading |
| // of the stream or discard incoming data without further processing." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2-7 |
| // |
| // We should send the H3_STREAM_CREATION_ERROR error code, |
| // but the quic package currently doesn't allow setting error codes |
| // for STOP_SENDING frames. |
| // TODO: Should CloseRead take an error code? |
| st.stream.CloseRead() |
| err = nil |
| } |
| if err == io.EOF { |
| err = &quic.ApplicationError{ |
| Code: uint64(errH3ClosedCriticalStream), |
| Reason: streamType(stype).String() + " stream closed", |
| } |
| } |
| if err != nil { |
| cc.qconn.Abort(err) |
| } |
| } |
| |
| func (cc *ClientConn) checkStreamCreation(stype streamType, name string) error { |
| cc.mu.Lock() |
| defer cc.mu.Unlock() |
| bit := uint8(1) << stype |
| if cc.streamsCreated&bit != 0 { |
| return &quic.ApplicationError{ |
| Code: uint64(errH3StreamCreationError), |
| Reason: "multiple " + name + " streams created", |
| } |
| } |
| cc.streamsCreated |= bit |
| return nil |
| } |
| |
| func (cc *ClientConn) handleControlStream(st *stream) error { |
| if err := cc.checkStreamCreation(streamTypeControl, "control"); err != nil { |
| // "[...] receipt of a second stream claiming to be a control stream |
| // MUST be treated as a connection error of type H3_STREAM_CREATION_ERROR." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.1-2 |
| return err |
| } |
| |
| // "A SETTINGS frame MUST be sent as the first frame of each control stream [...]" |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-2 |
| if err := st.readSettings(func(settingsType, settingsValue int64) error { |
| switch settingsType { |
| case settingsMaxFieldSectionSize: |
| _ = settingsValue // TODO |
| case settingsQPACKMaxTableCapacity: |
| _ = settingsValue // TODO |
| case settingsQPACKBlockedStreams: |
| _ = settingsValue // TODO |
| default: |
| // Unknown settings types are ignored. |
| } |
| return nil |
| }); err != nil { |
| return err |
| } |
| |
| for { |
| ftype, err := st.readFrameHeader() |
| if err != nil { |
| return err |
| } |
| switch ftype { |
| case frameTypeCancelPush: |
| // "If a CANCEL_PUSH frame is received that references a push ID |
| // greater than currently allowed on the connection, |
| // this MUST be treated as a connection error of type H3_ID_ERROR." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3-7 |
| return &quic.ApplicationError{ |
| Code: uint64(errH3IDError), |
| Reason: "CANCEL_PUSH received when no MAX_PUSH_ID has been sent", |
| } |
| case frameTypeGoaway: |
| // TODO: Wait for requests to complete before closing connection. |
| return errH3NoError |
| default: |
| // Unknown frames are ignored. |
| if err := st.discardUnknownFrame(ftype); err != nil { |
| return err |
| } |
| } |
| } |
| } |
| |
| func (cc *ClientConn) handleEncoderStream(*stream) error { |
| if err := cc.checkStreamCreation(streamTypeEncoder, "encoder"); err != nil { |
| // "Receipt of a second instance of [an encoder stream] MUST |
| // be treated as a connection error of type H3_STREAM_CREATION_ERROR." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.1-2 |
| return err |
| } |
| // TODO |
| return nil |
| } |
| |
| func (cc *ClientConn) handleDecoderStream(*stream) error { |
| if err := cc.checkStreamCreation(streamTypeDecoder, "decoder"); err != nil { |
| // "Receipt of a second instance of [a decoder stream] MUST |
| // be treated as a connection error of type H3_STREAM_CREATION_ERROR." |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-6.2.1-2 |
| return err |
| } |
| // TODO |
| return nil |
| } |
| |
| func (cc *ClientConn) handlePushStream(*stream) error { |
| // "A client MUST treat receipt of a push stream as a connection error |
| // of type H3_ID_ERROR when no MAX_PUSH_ID frame has been sent [...]" |
| // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.6-3 |
| return &quic.ApplicationError{ |
| Code: uint64(errH3IDError), |
| Reason: "push stream created when no MAX_PUSH_ID has been sent", |
| } |
| } |