2022-07-21 18:50:10 +02:00
package http3
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
2023-02-02 12:42:11 +01:00
"net"
2022-07-21 18:50:10 +02:00
"net/http"
"strings"
"sync"
2023-07-03 12:37:42 +02:00
"sync/atomic"
2022-07-21 18:50:10 +02:00
"golang.org/x/net/http/httpguts"
2023-02-02 12:42:11 +01:00
"github.com/quic-go/quic-go"
2022-07-21 18:50:10 +02:00
)
2024-03-26 19:56:06 +01:00
// Settings are HTTP/3 settings that apply to the underlying connection.
type Settings struct {
// Support for HTTP/3 datagrams (RFC 9297)
EnableDatagram bool
// Extended CONNECT, RFC 9220
EnableExtendedConnect bool
// Other settings, defined by the application
Other map [ uint64 ] uint64
}
// RoundTripOpt are options for the Transport.RoundTripOpt method.
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
}
2022-07-21 18:50:10 +02:00
type roundTripCloser interface {
RoundTripOpt ( * http . Request , RoundTripOpt ) ( * http . Response , error )
2023-02-02 12:42:11 +01:00
HandshakeComplete ( ) bool
2022-07-21 18:50:10 +02:00
io . Closer
}
2023-07-03 12:37:42 +02:00
type roundTripCloserWithCount struct {
roundTripCloser
useCount atomic . Int64
}
2022-07-21 18:50:10 +02:00
// 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.
// If nil, reasonable default values will be used.
QuicConfig * quic . Config
2024-03-26 19:56:06 +01:00
// 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.
2022-07-21 18:50:10 +02:00
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 )
// Dial specifies an optional dial function for creating QUIC
// connections for requests.
2023-05-01 16:05:02 +02:00
// If Dial is nil, a UDPConn will be created at the first request
// and will be reused for subsequent connections to other servers.
2022-07-21 18:50:10 +02:00
Dial func ( ctx context . Context , addr string , tlsCfg * tls . Config , cfg * quic . Config ) ( quic . EarlyConnection , error )
// 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
2023-02-02 12:42:11 +01:00
newClient func ( hostname string , tlsConf * tls . Config , opts * roundTripperOpts , conf * quic . Config , dialer dialFunc ) ( roundTripCloser , error ) // so we can mock it in tests
2023-07-03 12:37:42 +02:00
clients map [ string ] * roundTripCloserWithCount
transport * quic . Transport
2022-07-21 18:50:10 +02:00
}
var (
_ http . RoundTripper = & RoundTripper { }
_ io . Closer = & RoundTripper { }
)
// ErrNoCachedConn is returned when RoundTripper.OnlyCachedConn is set
var ErrNoCachedConn = errors . New ( "http3: no cached connection was available" )
// RoundTripOpt is like RoundTrip, but takes options.
func ( r * RoundTripper ) RoundTripOpt ( req * http . Request , opt RoundTripOpt ) ( * http . Response , error ) {
if req . URL == nil {
closeRequestBody ( req )
return nil , errors . New ( "http3: nil Request.URL" )
}
2023-05-01 16:05:02 +02:00
if req . URL . Scheme != "https" {
closeRequestBody ( req )
return nil , fmt . Errorf ( "http3: unsupported protocol scheme: %s" , req . URL . Scheme )
}
2022-07-21 18:50:10 +02:00
if req . URL . Host == "" {
closeRequestBody ( req )
return nil , errors . New ( "http3: no Host in request URL" )
}
if req . Header == nil {
closeRequestBody ( req )
return nil , errors . New ( "http3: nil Request.Header" )
}
2023-02-02 12:42:11 +01:00
for k , vv := range req . Header {
if ! httpguts . ValidHeaderFieldName ( k ) {
return nil , fmt . Errorf ( "http3: invalid http header field name %q" , k )
}
for _ , v := range vv {
if ! httpguts . ValidHeaderFieldValue ( v ) {
return nil , fmt . Errorf ( "http3: invalid http header field value %q for key %v" , v , k )
}
}
}
2022-07-21 18:50:10 +02:00
if req . Method != "" && ! validMethod ( req . Method ) {
closeRequestBody ( req )
return nil , fmt . Errorf ( "http3: invalid method %q" , req . Method )
}
hostname := authorityAddr ( "https" , hostnameFromRequest ( req ) )
2023-02-02 12:42:11 +01:00
cl , isReused , err := r . getClient ( hostname , opt . OnlyCachedConn )
2022-07-21 18:50:10 +02:00
if err != nil {
return nil , err
}
2023-07-03 12:37:42 +02:00
defer cl . useCount . Add ( - 1 )
2023-02-02 12:42:11 +01:00
rsp , err := cl . RoundTripOpt ( req , opt )
if err != nil {
r . removeClient ( hostname )
if isReused {
if nerr , ok := err . ( net . Error ) ; ok && nerr . Timeout ( ) {
return r . RoundTripOpt ( req , opt )
}
}
}
return rsp , err
2022-07-21 18:50:10 +02:00
}
// RoundTrip does a round trip.
func ( r * RoundTripper ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
return r . RoundTripOpt ( req , RoundTripOpt { } )
}
2023-07-03 12:37:42 +02:00
func ( r * RoundTripper ) getClient ( hostname string , onlyCached bool ) ( rtc * roundTripCloserWithCount , isReused bool , err error ) {
2022-07-21 18:50:10 +02:00
r . mutex . Lock ( )
defer r . mutex . Unlock ( )
if r . clients == nil {
2023-07-03 12:37:42 +02:00
r . clients = make ( map [ string ] * roundTripCloserWithCount )
2022-07-21 18:50:10 +02:00
}
client , ok := r . clients [ hostname ]
if ! ok {
if onlyCached {
2023-02-02 12:42:11 +01:00
return nil , false , ErrNoCachedConn
2022-07-21 18:50:10 +02:00
}
var err error
2023-02-02 12:42:11 +01:00
newCl := newClient
if r . newClient != nil {
newCl = r . newClient
}
2023-05-01 16:05:02 +02:00
dial := r . Dial
if dial == nil {
2023-07-03 12:37:42 +02:00
if r . transport == nil {
udpConn , err := net . ListenUDP ( "udp" , nil )
2023-05-01 16:05:02 +02:00
if err != nil {
return nil , false , err
}
2023-07-03 12:37:42 +02:00
r . transport = & quic . Transport { Conn : udpConn }
2023-05-01 16:05:02 +02:00
}
dial = r . makeDialer ( )
}
2023-07-03 12:37:42 +02:00
c , err := newCl (
2022-07-21 18:50:10 +02:00
hostname ,
r . TLSClientConfig ,
& roundTripperOpts {
EnableDatagram : r . EnableDatagrams ,
DisableCompression : r . DisableCompression ,
MaxHeaderBytes : r . MaxResponseHeaderBytes ,
StreamHijacker : r . StreamHijacker ,
UniStreamHijacker : r . UniStreamHijacker ,
2024-01-18 23:47:00 +01:00
AdditionalSettings : r . AdditionalSettings ,
2022-07-21 18:50:10 +02:00
} ,
r . QuicConfig ,
2023-05-01 16:05:02 +02:00
dial ,
2022-07-21 18:50:10 +02:00
)
if err != nil {
2023-02-02 12:42:11 +01:00
return nil , false , err
2022-07-21 18:50:10 +02:00
}
2023-07-03 12:37:42 +02:00
client = & roundTripCloserWithCount { roundTripCloser : c }
2022-07-21 18:50:10 +02:00
r . clients [ hostname ] = client
2023-02-02 12:42:11 +01:00
} else if client . HandshakeComplete ( ) {
isReused = true
}
2023-07-03 12:37:42 +02:00
client . useCount . Add ( 1 )
2023-02-02 12:42:11 +01:00
return client , isReused , nil
}
func ( r * RoundTripper ) removeClient ( hostname string ) {
r . mutex . Lock ( )
defer r . mutex . Unlock ( )
if r . clients == nil {
return
2022-07-21 18:50:10 +02:00
}
2023-02-02 12:42:11 +01:00
delete ( r . clients , hostname )
2022-07-21 18:50:10 +02:00
}
2023-05-01 16:05:02 +02:00
// Close closes the QUIC connections that this RoundTripper has used.
// It also closes the underlying UDPConn if it is not nil.
2022-07-21 18:50:10 +02:00
func ( r * RoundTripper ) Close ( ) error {
r . mutex . Lock ( )
defer r . mutex . Unlock ( )
for _ , client := range r . clients {
if err := client . Close ( ) ; err != nil {
return err
}
}
r . clients = nil
2023-07-03 12:37:42 +02:00
if r . transport != nil {
if err := r . transport . Close ( ) ; err != nil {
return err
}
if err := r . transport . Conn . Close ( ) ; err != nil {
return err
}
r . transport = nil
2023-05-01 16:05:02 +02:00
}
2022-07-21 18:50:10 +02:00
return nil
}
func closeRequestBody ( req * http . Request ) {
if req . Body != nil {
req . Body . Close ( )
}
}
func validMethod ( method string ) bool {
/ *
Method = "OPTIONS" ; Section 9.2
| "GET" ; Section 9.3
| "HEAD" ; Section 9.4
| "POST" ; Section 9.5
| "PUT" ; Section 9.6
| "DELETE" ; Section 9.7
| "TRACE" ; Section 9.8
| "CONNECT" ; Section 9.9
| extension - method
extension - method = token
token = 1 * < any CHAR except CTLs or separators >
* /
return len ( method ) > 0 && strings . IndexFunc ( method , isNotToken ) == - 1
}
// copied from net/http/http.go
func isNotToken ( r rune ) bool {
return ! httpguts . IsTokenRune ( r )
}
2023-05-01 16:05:02 +02:00
// 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
}
2023-07-03 12:37:42 +02:00
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 ( )
delete ( r . clients , hostname )
}
2023-05-01 16:05:02 +02:00
}
}