diff --git a/connection.go b/connection.go index 794c881..6d8d749 100644 --- a/connection.go +++ b/connection.go @@ -4,6 +4,9 @@ import ( "context" "crypto/x509" "errors" + "github.com/Noooste/fhttp/http2" + "github.com/Noooste/quic-go" + "github.com/Noooste/quic-go/http3" tls "github.com/Noooste/utls" "net" "time" @@ -42,7 +45,89 @@ func (s *Session) dial(ctx context.Context, network, addr string) (net.Conn, err } func (s *Session) upgradeTLS(ctx context.Context, conn net.Conn, addr string) (net.Conn, error) { - // Split addr and port + if !s.InsecureSkipVerify { + if err := s.Pin(addr); err != nil { + return nil, errors.New("failed to pin: " + err.Error()) + } + } + + config, err := s.getTLSConfig(addr) + if err != nil { + return nil, err + } + + tlsConn := tls.UClient(conn, config, tls.HelloCustom) + + var fn = s.GetClientHelloSpec + if fn == nil { + fn = GetBrowserClientHelloFunc(s.Browser) + } + + specs := fn() + + if v, k := ctx.Value(forceHTTP1Key).(bool); k && v { + for _, ext := range specs.Extensions { + switch ext.(type) { + case *tls.ALPNExtension: + ext.(*tls.ALPNExtension).AlpnProtocols = []string{"http/1.1"} + } + } + + config.NextProtos = []string{"http/1.1"} + } + + if err = tlsConn.ApplyPreset(specs); err != nil { + return nil, errors.New("failed to apply preset: " + err.Error()) + } + + if err = tlsConn.Handshake(); err != nil { + return nil, errors.New("failed to handshake: " + err.Error()) + } + + return tlsConn.Conn, nil +} + +func (s *Session) http3Dialer(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) { + udpConn, err := net.ListenUDP("udp", nil) + if err != nil { + return nil, err + } + + s.quicTransport = &quic.Transport{ + Conn: udpConn, + } + + var fn = s.GetClientHelloSpec + if fn == nil { + fn = GetBrowserClientHelloFunc(s.Browser) + } + + tlsCfg, err = s.getTLSConfig(addr) + if err != nil { + return nil, err + } + + tlsCfg.ClientHelloSpec = fn() + + udpAddr, err := net.ResolveUDPAddr("udp", addr) + + if err != nil { + return nil, errors.New("failed to resolve udp addr: " + err.Error()) + } + + for _, ext := range tlsCfg.ClientHelloSpec.Extensions { + switch ext.(type) { + case *tls.ALPNExtension: + ext.(*tls.ALPNExtension).AlpnProtocols = []string{http3.NextProtoH3, http2.NextProtoTLS, "http/1.1"} + } + } + + tlsCfg.NextProtos = []string{http3.NextProtoH3, http2.NextProtoTLS, "http/1.1"} + + return s.quicTransport.DialEarly(ctx, udpAddr, tlsCfg, cfg) +} + +func (s *Session) getTLSConfig(addr string) (*tls.Config, error) { hostname, _, err := net.SplitHostPort(addr) if err != nil { @@ -104,33 +189,5 @@ func (s *Session) upgradeTLS(ctx context.Context, conn net.Conn, addr string) (n }, } - tlsConn := tls.UClient(conn, &config, tls.HelloCustom) - - var fn = s.GetClientHelloSpec - if fn == nil { - fn = GetBrowserClientHelloFunc(s.Browser) - } - - specs := fn() - - if v, k := ctx.Value(forceHTTP1Key).(bool); k && v { - for _, ext := range specs.Extensions { - switch ext.(type) { - case *tls.ALPNExtension: - ext.(*tls.ALPNExtension).AlpnProtocols = []string{"http/1.1"} - } - } - - config.NextProtos = []string{"http/1.1"} - } - - if err = tlsConn.ApplyPreset(specs); err != nil { - return nil, errors.New("failed to apply preset: " + err.Error()) - } - - if err = tlsConn.Handshake(); err != nil { - return nil, errors.New("failed to handshake: " + err.Error()) - } - - return tlsConn.Conn, nil + return &config, nil } diff --git a/go.mod b/go.mod index a38273d..7e052ad 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,29 @@ go 1.22.0 require ( github.com/Noooste/fhttp v1.0.12 - github.com/Noooste/utls v1.2.12 + github.com/Noooste/utls v1.3.1 github.com/Noooste/websocket v1.0.3 github.com/fatih/color v1.18.0 golang.org/x/net v0.34.0 ) require ( + github.com/Noooste/quic-go v0.0.2 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/cloudflare/circl v1.5.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 02356eb..5ab5548 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,31 @@ github.com/Noooste/fhttp v1.0.12 h1:2N15bIATKaC6q+LVyRGyxPyuqEPvwAS3Uk1peC3YVHU= github.com/Noooste/fhttp v1.0.12/go.mod h1:CMVxKOhNheqJN5HYE4Rlvz2SRdV8Uv7YWmi6OwmB/Bk= +github.com/Noooste/quic-go v0.0.1 h1:ohiDAaztc0wnJ1PowvrmjZPS3e+ouz/bqRFvWvzNbwU= +github.com/Noooste/quic-go v0.0.1/go.mod h1:mpECe+JXUwCSmakFAcYZ4PlS9BdE1mgwELItwp+8ALc= +github.com/Noooste/quic-go v0.0.2 h1:02CBSGnl5ofrQBmu2jRtWzCiXvL9TQhAwL+2bW+TAaQ= +github.com/Noooste/quic-go v0.0.2/go.mod h1:mpECe+JXUwCSmakFAcYZ4PlS9BdE1mgwELItwp+8ALc= github.com/Noooste/utls v1.2.12 h1:Zcm/7OB6W4Ro1q2OV1BrFb3qBI7uqYeC21wHYX+Ez9I= github.com/Noooste/utls v1.2.12/go.mod h1:CJaLzDHOhjuKESY3/wTSEzs3N2QgdXTrNQE3sW2632M= +github.com/Noooste/utls v1.3.1 h1:oI8kLnpmOnqKSJLifSz+G7UoXyTBFiw0kXTWMyhKHuw= +github.com/Noooste/utls v1.3.1/go.mod h1:CJaLzDHOhjuKESY3/wTSEzs3N2QgdXTrNQE3sW2632M= github.com/Noooste/websocket v1.0.3 h1:drW7tvZ3YqzqI9wApnaH1Q0syFMXO7gbLlsBWjZvMNA= github.com/Noooste/websocket v1.0.3/go.mod h1:Qhw0Rtuju/fPPbcb3R5XGq7poa51qPDL462jTltl9nQ= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -17,15 +33,35 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/request.go b/request.go index bd3758f..87db0c4 100644 --- a/request.go +++ b/request.go @@ -122,6 +122,11 @@ func (s *Session) buildRequest(ctx context.Context, req *Request) (err error) { req.formatHeader() + if s.UseHTTP3 { + req.HttpRequest.Header.Del(http.PHeaderOrderKey) + req.HttpRequest.Header.Del(http.HeaderOrderKey) + } + if !req.NoCookie { cookies := s.CookieJar.Cookies(req.HttpRequest.URL) if cookies != nil && len(cookies) > 0 { diff --git a/session.go b/session.go index 590d9e1..89f6ba7 100644 --- a/session.go +++ b/session.go @@ -150,7 +150,12 @@ func (s *Session) send(request *Request) (response *Response, err error) { return nil, err } - roundTripper = s.Transport + if s.UseHTTP3 { + roundTripper = s.HTTP3Transport + } else { + roundTripper = s.Transport + } + s.logRequest(request) if request.ForceHTTP1 { diff --git a/structs.go b/structs.go index 7af5883..ed2eb39 100644 --- a/structs.go +++ b/structs.go @@ -5,6 +5,8 @@ import ( "errors" http "github.com/Noooste/fhttp" "github.com/Noooste/fhttp/http2" + "github.com/Noooste/quic-go" + "github.com/Noooste/quic-go/http3" tls "github.com/Noooste/utls" "io" "net" @@ -42,8 +44,12 @@ type Session struct { // Name or identifier of the browser used in the session. Browser string - HTTP2Transport *http2.Transport Transport *http.Transport + HTTP2Transport *http2.Transport + + UseHTTP3 bool + HTTP3Transport *http3.Transport + quicTransport *quic.Transport // Function to provide custom TLS handshake details. GetClientHelloSpec func() *tls.ClientHelloSpec diff --git a/test/session_test.go b/test/session_test.go index 10d4b75..c610469 100644 --- a/test/session_test.go +++ b/test/session_test.go @@ -670,3 +670,18 @@ func TestSession_Timeout2(t *testing.T) { return } } + +func TestSession_HTTP3(t *testing.T) { + session := azuretls.NewSession() + defer session.Close() + + session.UseHTTP3 = true + + resp, err := session.Get("https://www.google.com/") + + if err != nil { + t.Fatal(err) + } + + fmt.Print(resp.Status) +} diff --git a/transport.go b/transport.go index afaae48..91cf5cf 100644 --- a/transport.go +++ b/transport.go @@ -3,6 +3,7 @@ package azuretls import ( http "github.com/Noooste/fhttp" "github.com/Noooste/fhttp/http2" + "github.com/Noooste/quic-go/http3" "time" ) @@ -20,6 +21,12 @@ func (s *Session) InitTransport(browser string) (err error) { } } + if s.HTTP3Transport == nil { + if err = s.initHTTP3(browser); err != nil { + return + } + } + return } @@ -69,3 +76,11 @@ func (s *Session) initHTTP2(browser string) error { return nil } + +func (s *Session) initHTTP3(browser string) error { + s.HTTP3Transport = &http3.Transport{ + Dial: s.http3Dialer, + } + + return nil +}