diff --git a/common/httpx/httpx.go b/common/httpx/httpx.go index f07c9c64..155c28f7 100644 --- a/common/httpx/httpx.go +++ b/common/httpx/httpx.go @@ -140,25 +140,46 @@ func New(options *Options) (*HTTPX, error) { return nil } } + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + } + if httpx.Options.SniName != "" { + tlsConfig.ServerName = httpx.Options.SniName + } + httpx.Options.Protocol = Proto(strings.ToLower(string(httpx.Options.Protocol))) + if httpx.Options.Protocol == HTTP3 { + gologger.Warning().Msg("HTTP/3 is experimental and currently falls back to default TLS negotiation") + } + switch httpx.Options.Protocol { + case HTTP11: + tlsConfig.NextProtos = []string{"http/1.1"} + case HTTP2: + tlsConfig.NextProtos = []string{"h2"} + default: + tlsConfig.NextProtos = []string{"h2", "http/1.1"} + } + transport := &http.Transport{ DialContext: httpx.Dialer.Dial, DialTLSContext: httpx.buildTLSDialer(options), MaxIdleConnsPerHost: -1, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS10, - }, - DisableKeepAlives: true, + TLSClientConfig: tlsConfig, + DisableKeepAlives: true, } if httpx.Options.Protocol == HTTP11 { // disable http2 _ = os.Setenv("GODEBUG", "http2client=0") + var p http.Protocols + p.SetHTTP1(true) + p.SetHTTP2(false) + transport.Protocols = &p transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} - } - - if httpx.Options.SniName != "" { - transport.TLSClientConfig.ServerName = httpx.Options.SniName + } else if httpx.Options.Protocol == HTTP2 { + var p http.Protocols + p.SetHTTP2(true) + transport.Protocols = &p } if httpx.Options.HTTPProxy != "" { @@ -190,15 +211,21 @@ func New(options *Options) (*HTTPX, error) { var protocols http.Protocols protocols.SetHTTP2(true) protocols.SetUnencryptedHTTP2(true) - transport2 := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - MinVersion: tls.VersionTLS10, - }, - Protocols: &protocols, + tlsConfig2 := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + NextProtos: []string{"h2"}, } if httpx.Options.SniName != "" { - transport2.TLSClientConfig.ServerName = httpx.Options.SniName + tlsConfig2.ServerName = httpx.Options.SniName + } + client2Opts := *options + client2Opts.Protocol = HTTP2 + transport2 := &http.Transport{ + DialContext: httpx.Dialer.Dial, + DialTLSContext: httpx.buildTLSDialer(&client2Opts), + TLSClientConfig: tlsConfig2, + Protocols: &protocols, } httpx.client2 = &http.Client{ Transport: transport2, @@ -220,14 +247,28 @@ func New(options *Options) (*HTTPX, error) { } func (h *HTTPX) buildTLSDialer(options *Options) func(ctx context.Context, network, addr string) (net.Conn, error) { + tlsCfg := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS10, + } + if options.SniName != "" { + tlsCfg.ServerName = options.SniName + } + switch options.Protocol { + case HTTP11: + tlsCfg.NextProtos = []string{"http/1.1"} + case HTTP2: + tlsCfg.NextProtos = []string{"h2"} + default: + tlsCfg.NextProtos = []string{"h2", "http/1.1"} + } + if options.TlsImpersonate == "" { return func(ctx context.Context, network, addr string) (net.Conn, error) { - return h.Dialer.DialTLS(ctx, network, addr) + return h.Dialer.DialTLSWithConfig(ctx, network, addr, tlsCfg) } } - tlsCfg := &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS10} - strategy, identity := resolveImpersonateStrategy(options.TlsImpersonate) return func(ctx context.Context, network, addr string) (net.Conn, error) { diff --git a/common/httpx/httpx_test.go b/common/httpx/httpx_test.go index 9687d7d0..8a6fc5db 100644 --- a/common/httpx/httpx_test.go +++ b/common/httpx/httpx_test.go @@ -199,3 +199,25 @@ func TestDoSwitchingProtocolsDoesNotHang(t *testing.T) { t.Fatal("Do hung on 101 Switching Protocols response") } } + +func TestProtocolALPNConfiguration(t *testing.T) { + for _, tc := range []struct { + proto Proto + expectedProtos []string + }{ + {HTTP11, []string{"http/1.1"}}, + {HTTP2, []string{"h2"}}, + {UNKNOWN, []string{"h2", "http/1.1"}}, + } { + opts := DefaultOptions + opts.Protocol = tc.proto + ht, err := New(&opts) + require.NoError(t, err) + require.NotNil(t, ht) + tr, ok := ht.client.HTTPClient.Transport.(*http.Transport) + require.True(t, ok) + require.NotNil(t, tr.TLSClientConfig) + require.Equal(t, tc.expectedProtos, tr.TLSClientConfig.NextProtos) + } +} + diff --git a/runner/options.go b/runner/options.go index f636b9d1..31813e4d 100644 --- a/runner/options.go +++ b/runner/options.go @@ -871,7 +871,7 @@ func (options *Options) ValidateOptions() error { options.OutputCDN = "true" } - if !stringsutil.EqualFoldAny(options.Protocol, string(httpxcommon.UNKNOWN), string(httpxcommon.HTTP11)) { + if !stringsutil.EqualFoldAny(options.Protocol, string(httpxcommon.UNKNOWN), string(httpxcommon.HTTP11), string(httpxcommon.HTTP2), string(httpxcommon.HTTP3)) { return fmt.Errorf("invalid protocol: %s", options.Protocol) } diff --git a/runner/runner.go b/runner/runner.go index c903054d..2a526260 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -235,7 +235,7 @@ func New(options *Options) (*Runner, error) { } httpxOptions.Resolvers = options.Resolvers httpxOptions.TlsImpersonate = options.TlsImpersonate - httpxOptions.Protocol = httpx.Proto(options.Protocol) + httpxOptions.Protocol = httpx.Proto(strings.ToLower(options.Protocol)) var key, value string httpxOptions.CustomHeaders = make(map[string][]string) diff --git a/runner/runner_test.go b/runner/runner_test.go index 78b6726b..dc06ab7d 100644 --- a/runner/runner_test.go +++ b/runner/runner_test.go @@ -550,6 +550,18 @@ func TestStoreResponse_withMatcherSetsFlag(t *testing.T) { require.True(t, opts.HasMatcherOrFilter()) } +func TestProtocolValidation(t *testing.T) { + for _, proto := range []string{"", "http11", "http2", "http3", "HTTP11", "HTTP2", "HTTP3"} { + opts := &Options{Protocol: proto} + err := opts.ValidateOptions() + require.NoError(t, err, "protocol %s should be valid", proto) + } + invalidOpts := &Options{Protocol: "ftp"} + err := invalidOpts.ValidateOptions() + require.Error(t, err) +} + + func TestRunner_duplicate(t *testing.T) { const ( pageA = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n