Skip to content

Commit

Permalink
feat(http3): implement HTTP/3 support and connection management
Browse files Browse the repository at this point in the history
  • Loading branch information
Noooste committed Feb 1, 2025
1 parent db66bf0 commit 378ce85
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 33 deletions.
117 changes: 87 additions & 30 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
12 changes: 11 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
36 changes: 36 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,31 +1,67 @@
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=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
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=
5 changes: 5 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 7 additions & 1 deletion structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
15 changes: 15 additions & 0 deletions transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package azuretls
import (
http "github.com/Noooste/fhttp"
"github.com/Noooste/fhttp/http2"
"github.com/Noooste/quic-go/http3"
"time"
)

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

0 comments on commit 378ce85

Please sign in to comment.