github.com/sagernet/quic-go@v0.43.1-beta.1/http3/client.go (about)

     1  package http3
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptrace"
    10  	"net/textproto"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/quic-go/qpack"
    15  	"github.com/sagernet/quic-go"
    16  	"github.com/sagernet/quic-go/internal/protocol"
    17  	"github.com/sagernet/quic-go/quicvarint"
    18  	"golang.org/x/exp/slog"
    19  )
    20  
    21  const (
    22  	// MethodGet0RTT allows a GET request to be sent using 0-RTT.
    23  	// Note that 0-RTT doesn't provide replay protection and should only be used for idempotent requests.
    24  	MethodGet0RTT = "GET_0RTT"
    25  	// MethodHead0RTT allows a HEAD request to be sent using 0-RTT.
    26  	// Note that 0-RTT doesn't provide replay protection and should only be used for idempotent requests.
    27  	MethodHead0RTT = "HEAD_0RTT"
    28  )
    29  
    30  const (
    31  	defaultUserAgent              = "quic-go HTTP/3"
    32  	defaultMaxResponseHeaderBytes = 10 * 1 << 20 // 10 MB
    33  )
    34  
    35  var defaultQuicConfig = &quic.Config{
    36  	MaxIncomingStreams: -1, // don't allow the server to create bidirectional streams
    37  	KeepAlivePeriod:    10 * time.Second,
    38  }
    39  
    40  // SingleDestinationRoundTripper is an HTTP/3 client doing requests to a single remote server.
    41  type SingleDestinationRoundTripper struct {
    42  	Connection quic.Connection
    43  
    44  	// Enable support for HTTP/3 datagrams (RFC 9297).
    45  	// If a QUICConfig is set, datagram support also needs to be enabled on the QUIC layer by setting EnableDatagrams.
    46  	EnableDatagrams bool
    47  
    48  	// Additional HTTP/3 settings.
    49  	// It is invalid to specify any settings defined by RFC 9114 (HTTP/3) and RFC 9297 (HTTP Datagrams).
    50  	AdditionalSettings map[uint64]uint64
    51  	StreamHijacker     func(FrameType, quic.ConnectionTracingID, quic.Stream, error) (hijacked bool, err error)
    52  	UniStreamHijacker  func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)
    53  
    54  	// MaxResponseHeaderBytes specifies a limit on how many response bytes are
    55  	// allowed in the server's response header.
    56  	// Zero means to use a default limit.
    57  	MaxResponseHeaderBytes int64
    58  
    59  	// DisableCompression, if true, prevents the Transport from requesting compression with an
    60  	// "Accept-Encoding: gzip" request header when the Request contains no existing Accept-Encoding value.
    61  	// If the Transport requests gzip on its own and gets a gzipped response, it's transparently
    62  	// decoded in the Response.Body.
    63  	// However, if the user explicitly requested gzip it is not automatically uncompressed.
    64  	DisableCompression bool
    65  
    66  	Logger *slog.Logger
    67  
    68  	initOnce      sync.Once
    69  	hconn         *connection
    70  	requestWriter *requestWriter
    71  	decoder       *qpack.Decoder
    72  }
    73  
    74  var _ http.RoundTripper = &SingleDestinationRoundTripper{}
    75  
    76  func (c *SingleDestinationRoundTripper) Start() Connection {
    77  	c.initOnce.Do(func() { c.init() })
    78  	return c.hconn
    79  }
    80  
    81  func (c *SingleDestinationRoundTripper) init() {
    82  	c.decoder = qpack.NewDecoder(func(hf qpack.HeaderField) {})
    83  	c.requestWriter = newRequestWriter()
    84  	c.hconn = newConnection(c.Connection, c.EnableDatagrams, protocol.PerspectiveClient, c.Logger)
    85  	// send the SETTINGs frame, using 0-RTT data, if possible
    86  	go func() {
    87  		if err := c.setupConn(c.hconn); err != nil {
    88  			if c.Logger != nil {
    89  				c.Logger.Debug("Setting up connection failed", "error", err)
    90  			}
    91  			c.hconn.CloseWithError(quic.ApplicationErrorCode(ErrCodeInternalError), "")
    92  		}
    93  	}()
    94  	if c.StreamHijacker != nil {
    95  		go c.handleBidirectionalStreams()
    96  	}
    97  	go c.hconn.HandleUnidirectionalStreams(c.UniStreamHijacker)
    98  }
    99  
   100  func (c *SingleDestinationRoundTripper) setupConn(conn *connection) error {
   101  	// open the control stream
   102  	str, err := conn.OpenUniStream()
   103  	if err != nil {
   104  		return err
   105  	}
   106  	b := make([]byte, 0, 64)
   107  	b = quicvarint.Append(b, streamTypeControlStream)
   108  	// send the SETTINGS frame
   109  	b = (&settingsFrame{Datagram: c.EnableDatagrams, Other: c.AdditionalSettings}).Append(b)
   110  	_, err = str.Write(b)
   111  	return err
   112  }
   113  
   114  func (c *SingleDestinationRoundTripper) handleBidirectionalStreams() {
   115  	for {
   116  		str, err := c.hconn.AcceptStream(context.Background())
   117  		if err != nil {
   118  			if c.Logger != nil {
   119  				c.Logger.Debug("accepting bidirectional stream failed", "error", err)
   120  			}
   121  			return
   122  		}
   123  		go func(str quic.Stream) {
   124  			_, err := parseNextFrame(str, func(ft FrameType, e error) (processed bool, err error) {
   125  				id := c.hconn.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
   126  				return c.StreamHijacker(ft, id, str, e)
   127  			})
   128  			if err == errHijacked {
   129  				return
   130  			}
   131  			if err != nil {
   132  				if c.Logger != nil {
   133  					c.Logger.Debug("error handling stream", "error", err)
   134  				}
   135  			}
   136  			c.hconn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "received HTTP/3 frame on bidirectional stream")
   137  		}(str)
   138  	}
   139  }
   140  
   141  func (c *SingleDestinationRoundTripper) maxHeaderBytes() uint64 {
   142  	if c.MaxResponseHeaderBytes <= 0 {
   143  		return defaultMaxResponseHeaderBytes
   144  	}
   145  	return uint64(c.MaxResponseHeaderBytes)
   146  }
   147  
   148  // RoundTrip executes a request and returns a response
   149  func (c *SingleDestinationRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   150  	c.initOnce.Do(func() { c.init() })
   151  
   152  	rsp, err := c.roundTrip(req)
   153  	if err != nil && req.Context().Err() != nil {
   154  		// if the context was canceled, return the context cancellation error
   155  		err = req.Context().Err()
   156  	}
   157  	return rsp, err
   158  }
   159  
   160  func (c *SingleDestinationRoundTripper) roundTrip(req *http.Request) (*http.Response, error) {
   161  	// Immediately send out this request, if this is a 0-RTT request.
   162  	switch req.Method {
   163  	case MethodGet0RTT:
   164  		// don't modify the original request
   165  		reqCopy := *req
   166  		req = &reqCopy
   167  		req.Method = http.MethodGet
   168  	case MethodHead0RTT:
   169  		// don't modify the original request
   170  		reqCopy := *req
   171  		req = &reqCopy
   172  		req.Method = http.MethodHead
   173  	default:
   174  		// wait for the handshake to complete
   175  		earlyConn, ok := c.Connection.(quic.EarlyConnection)
   176  		if ok {
   177  			select {
   178  			case <-earlyConn.HandshakeComplete():
   179  			case <-req.Context().Done():
   180  				return nil, req.Context().Err()
   181  			}
   182  		}
   183  	}
   184  
   185  	// It is only possible to send an Extended CONNECT request once the SETTINGS were received.
   186  	// See section 3 of RFC 8441.
   187  	if isExtendedConnectRequest(req) {
   188  		connCtx := c.Connection.Context()
   189  		// wait for the server's SETTINGS frame to arrive
   190  		select {
   191  		case <-c.hconn.ReceivedSettings():
   192  		case <-connCtx.Done():
   193  			return nil, context.Cause(connCtx)
   194  		}
   195  		if !c.hconn.Settings().EnableExtendedConnect {
   196  			return nil, errors.New("http3: server didn't enable Extended CONNECT")
   197  		}
   198  	}
   199  
   200  	reqDone := make(chan struct{})
   201  	str, err := c.hconn.openRequestStream(req.Context(), c.requestWriter, reqDone, c.DisableCompression, c.maxHeaderBytes())
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	// Request Cancellation:
   207  	// This go routine keeps running even after RoundTripOpt() returns.
   208  	// It is shut down when the application is done processing the body.
   209  	done := make(chan struct{})
   210  	go func() {
   211  		defer close(done)
   212  		select {
   213  		case <-req.Context().Done():
   214  			str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestCanceled))
   215  			str.CancelRead(quic.StreamErrorCode(ErrCodeRequestCanceled))
   216  		case <-reqDone:
   217  		}
   218  	}()
   219  
   220  	rsp, err := c.doRequest(req, str)
   221  	if err != nil { // if any error occurred
   222  		close(reqDone)
   223  		<-done
   224  		return nil, maybeReplaceError(err)
   225  	}
   226  	return rsp, maybeReplaceError(err)
   227  }
   228  
   229  func (c *SingleDestinationRoundTripper) OpenRequestStream(ctx context.Context) (RequestStream, error) {
   230  	c.initOnce.Do(func() { c.init() })
   231  
   232  	return c.hconn.openRequestStream(ctx, c.requestWriter, nil, c.DisableCompression, c.maxHeaderBytes())
   233  }
   234  
   235  // cancelingReader reads from the io.Reader.
   236  // It cancels writing on the stream if any error other than io.EOF occurs.
   237  type cancelingReader struct {
   238  	r   io.Reader
   239  	str Stream
   240  }
   241  
   242  func (r *cancelingReader) Read(b []byte) (int, error) {
   243  	n, err := r.r.Read(b)
   244  	if err != nil && err != io.EOF {
   245  		r.str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestCanceled))
   246  	}
   247  	return n, err
   248  }
   249  
   250  func (c *SingleDestinationRoundTripper) sendRequestBody(str Stream, body io.ReadCloser, contentLength int64) error {
   251  	defer body.Close()
   252  	buf := make([]byte, bodyCopyBufferSize)
   253  	sr := &cancelingReader{str: str, r: body}
   254  	if contentLength == -1 {
   255  		_, err := io.CopyBuffer(str, sr, buf)
   256  		return err
   257  	}
   258  
   259  	// make sure we don't send more bytes than the content length
   260  	n, err := io.CopyBuffer(str, io.LimitReader(sr, contentLength), buf)
   261  	if err != nil {
   262  		return err
   263  	}
   264  	var extra int64
   265  	extra, err = io.CopyBuffer(io.Discard, sr, buf)
   266  	n += extra
   267  	if n > contentLength {
   268  		str.CancelWrite(quic.StreamErrorCode(ErrCodeRequestCanceled))
   269  		return fmt.Errorf("http: ContentLength=%d with Body length %d", contentLength, n)
   270  	}
   271  	return err
   272  }
   273  
   274  func (c *SingleDestinationRoundTripper) doRequest(req *http.Request, str *requestStream) (*http.Response, error) {
   275  	if err := str.SendRequestHeader(req); err != nil {
   276  		return nil, err
   277  	}
   278  	if req.Body == nil {
   279  		str.Close()
   280  	} else {
   281  		// send the request body asynchronously
   282  		go func() {
   283  			contentLength := int64(-1)
   284  			// According to the documentation for http.Request.ContentLength,
   285  			// a value of 0 with a non-nil Body is also treated as unknown content length.
   286  			if req.ContentLength > 0 {
   287  				contentLength = req.ContentLength
   288  			}
   289  			if err := c.sendRequestBody(str, req.Body, contentLength); err != nil {
   290  				if c.Logger != nil {
   291  					c.Logger.Debug("error writing request", "error", err)
   292  				}
   293  			}
   294  			str.Close()
   295  		}()
   296  	}
   297  
   298  	// copy from net/http: support 1xx responses
   299  	trace := httptrace.ContextClientTrace(req.Context())
   300  	num1xx := 0               // number of informational 1xx headers received
   301  	const max1xxResponses = 5 // arbitrary bound on number of informational responses
   302  
   303  	var res *http.Response
   304  	for {
   305  		var err error
   306  		res, err = str.ReadResponse()
   307  		if err != nil {
   308  			return nil, err
   309  		}
   310  		resCode := res.StatusCode
   311  		is1xx := 100 <= resCode && resCode <= 199
   312  		// treat 101 as a terminal status, see https://github.com/golang/go/issues/26161
   313  		is1xxNonTerminal := is1xx && resCode != http.StatusSwitchingProtocols
   314  		if is1xxNonTerminal {
   315  			num1xx++
   316  			if num1xx > max1xxResponses {
   317  				return nil, errors.New("http: too many 1xx informational responses")
   318  			}
   319  			if trace != nil && trace.Got1xxResponse != nil {
   320  				if err := trace.Got1xxResponse(resCode, textproto.MIMEHeader(res.Header)); err != nil {
   321  					return nil, err
   322  				}
   323  			}
   324  			continue
   325  		}
   326  		break
   327  	}
   328  	connState := c.hconn.ConnectionState().TLS
   329  	res.TLS = &connState
   330  	res.Request = req
   331  	return res, nil
   332  }