diff --git a/js/modules/k6/http/request_test.go b/js/modules/k6/http/request_test.go index d2d585b374d..9e0660fd03b 100644 --- a/js/modules/k6/http/request_test.go +++ b/js/modules/k6/http/request_test.go @@ -5,11 +5,21 @@ import ( "compress/gzip" "compress/zlib" "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "io" "io/ioutil" + "math/big" + "net" "net/http" "net/http/cookiejar" + "net/http/httptest" "net/url" "runtime" "strconv" @@ -30,6 +40,7 @@ import ( "go.k6.io/k6/js/modulestest" "go.k6.io/k6/lib" + "go.k6.io/k6/lib/netext" "go.k6.io/k6/lib/testutils" "go.k6.io/k6/lib/testutils/httpmultibin" "go.k6.io/k6/metrics" @@ -2337,54 +2348,131 @@ func TestErrorsWithDecompression(t *testing.T) { } func TestRequestAndBatchTLS(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip() - } t.Parallel() - _, _, samples, rt, _ := newRuntime(t) //nolint:dogsled t.Run("cert_expired", func(t *testing.T) { - _, err := rt.RunString(`http.get("https://expired.badssl.com/");`) + t.Parallel() + _, state, _, rt, _ := newRuntime(t) + cert, key := GenerateTLSCertificate(t, "expired.localhost", time.Now().Add(-time.Hour), 0) + s, client := GetTestServerWithCertificate(t, cert, key) + go func() { + _ = s.Config.Serve(s.Listener) + }() + t.Cleanup(func() { + require.NoError(t, s.Config.Close()) + }) + host, port, err := net.SplitHostPort(s.Listener.Addr().String()) + require.NoError(t, err) + ip := net.ParseIP(host) + mybadsslHostname, err := lib.NewHostAddress(ip, port) + require.NoError(t, err) + state.Transport = client.Transport + state.TLSConfig = s.TLS + state.Dialer = &netext.Dialer{Hosts: map[string]*lib.HostAddress{"expired.localhost": mybadsslHostname}} + client.Transport.(*http.Transport).DialContext = state.Dialer.DialContext //nolint:forcetypeassert + _, err = rt.RunString(`throw JSON.stringify(http.get("https://expired.localhost/"));`) require.Error(t, err) assert.Contains(t, err.Error(), "x509: certificate has expired or is not yet valid") }) tlsVersionTests := []struct { Name, URL, Version string }{ - {Name: "tls10", URL: "https://tls-v1-0.badssl.com:1010/", Version: "http.TLS_1_0"}, - {Name: "tls11", URL: "https://tls-v1-1.badssl.com:1011/", Version: "http.TLS_1_1"}, - {Name: "tls12", URL: "https://badssl.com/", Version: "http.TLS_1_2"}, + {Name: "tls10", URL: "tlsv10.localhost", Version: "http.TLS_1_0"}, + {Name: "tls11", URL: "tlsv11.localhost", Version: "http.TLS_1_1"}, + {Name: "tls12", URL: "tlsv12.localhost", Version: "http.TLS_1_2"}, } for _, versionTest := range tlsVersionTests { versionTest := versionTest t.Run(versionTest.Name, func(t *testing.T) { - _, err := rt.RunString(fmt.Sprintf(` - var res = http.get("%s"); + t.Parallel() + _, state, samples, rt, _ := newRuntime(t) + cert, key := GenerateTLSCertificate(t, versionTest.URL, time.Now(), time.Hour) + s, client := GetTestServerWithCertificate(t, cert, key) + + switch versionTest.Name { + case "tls10": + s.TLS.MaxVersion = tls.VersionTLS10 + case "tls11": + s.TLS.MaxVersion = tls.VersionTLS11 + case "tls12": + s.TLS.MaxVersion = tls.VersionTLS12 + default: + panic(versionTest.Name + " unsupported") + } + go func() { + _ = s.Config.Serve(s.Listener) + }() + t.Cleanup(func() { + require.NoError(t, s.Config.Close()) + }) + host, port, err := net.SplitHostPort(s.Listener.Addr().String()) + require.NoError(t, err) + ip := net.ParseIP(host) + mybadsslHostname, err := lib.NewHostAddress(ip, port) + require.NoError(t, err) + state.Dialer = &netext.Dialer{Hosts: map[string]*lib.HostAddress{ + versionTest.URL: mybadsslHostname, + }} + state.Transport = client.Transport + state.TLSConfig = s.TLS + client.Transport.(*http.Transport).DialContext = state.Dialer.DialContext //nolint:forcetypeassert + realURL := "https://" + versionTest.URL + "/" + _, err = rt.RunString(fmt.Sprintf(` + var res = http.get("%s"); if (res.tls_version != %s) { throw new Error("wrong TLS version: " + res.tls_version); } - `, versionTest.URL, versionTest.Version)) + `, realURL, versionTest.Version)) assert.NoError(t, err) - assertRequestMetricsEmitted(t, metrics.GetBufferedSamples(samples), "GET", versionTest.URL, "", 200, "") + assertRequestMetricsEmitted(t, metrics.GetBufferedSamples(samples), "GET", realURL, "", 200, "") }) } tlsCipherSuiteTests := []struct { Name, URL, CipherSuite string + suite uint16 }{ - {Name: "cipher_suite_cbc", URL: "https://cbc.badssl.com/", CipherSuite: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"}, - {Name: "cipher_suite_ecc384", URL: "https://ecc384.badssl.com/", CipherSuite: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}, + {Name: "cipher_suite_cbc", URL: "cbc.localhost", CipherSuite: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", suite: tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA}, // TODO fix this to RSA instead of ECDSA + {Name: "cipher_suite_ecc384", URL: "ecc384.localhost", CipherSuite: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", suite: tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, } for _, cipherSuiteTest := range tlsCipherSuiteTests { cipherSuiteTest := cipherSuiteTest t.Run(cipherSuiteTest.Name, func(t *testing.T) { - _, err := rt.RunString(fmt.Sprintf(` + t.Parallel() + _, state, samples, rt, _ := newRuntime(t) + cert, key := GenerateTLSCertificate(t, cipherSuiteTest.URL, time.Now(), time.Hour) + s, client := GetTestServerWithCertificate(t, cert, key, cipherSuiteTest.suite) + go func() { + _ = s.Config.Serve(s.Listener) + }() + t.Cleanup(func() { + require.NoError(t, s.Config.Close()) + }) + host, port, err := net.SplitHostPort(s.Listener.Addr().String()) + require.NoError(t, err) + ip := net.ParseIP(host) + mybadsslHostname, err := lib.NewHostAddress(ip, port) + require.NoError(t, err) + state.Dialer = &netext.Dialer{Hosts: map[string]*lib.HostAddress{ + cipherSuiteTest.URL: mybadsslHostname, + }} + state.Transport = client.Transport + state.TLSConfig = s.TLS + client.Transport.(*http.Transport).DialContext = state.Dialer.DialContext //nolint:forcetypeassert + realURL := "https://" + cipherSuiteTest.URL + "/" + _, err = rt.RunString(fmt.Sprintf(` var res = http.get("%s"); if (res.tls_cipher_suite != "%s") { throw new Error("wrong TLS cipher suite: " + res.tls_cipher_suite); } - `, cipherSuiteTest.URL, cipherSuiteTest.CipherSuite)) + `, realURL, cipherSuiteTest.CipherSuite)) assert.NoError(t, err) - assertRequestMetricsEmitted(t, metrics.GetBufferedSamples(samples), "GET", cipherSuiteTest.URL, "", 200, "") + assertRequestMetricsEmitted(t, metrics.GetBufferedSamples(samples), "GET", realURL, "", 200, "") }) } t.Run("ocsp_stapled_good", func(t *testing.T) { + t.Parallel() + if runtime.GOOS == "windows" { + t.Skip("this doesn't work on windows for some reason") + } website := "https://www.wikipedia.org/" + tb, state, samples, rt, _ := newRuntime(t) + state.Dialer = tb.Dialer _, err := rt.RunString(fmt.Sprintf(` var res = http.request("GET", "%s"); if (res.ocsp.status != http.OCSP_STATUS_GOOD) { throw new Error("wrong ocsp stapled response status: " + res.ocsp.status); } @@ -2439,3 +2527,124 @@ func TestBinaryResponseWithStatus0(t *testing.T) { `) require.NoError(t, err) } + +func GenerateTLSCertificate(t *testing.T, host string, notBefore time.Time, validFor time.Duration) ([]byte, []byte) { + priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + // priv, err := ecdsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + keyUsage |= x509.KeyUsageKeyEncipherment + + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + SignatureAlgorithm: x509.ECDSAWithSHA256, + } + + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + require.NoError(t, err) + + certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + require.NoError(t, err) + + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + require.NoError(t, err) + keyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + require.NoError(t, err) + return certPem, keyPem +} + +func GetTestServerWithCertificate(t *testing.T, certPem, key []byte, suitesIds ...uint16) (*httptest.Server, *http.Client) { + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }), + ReadHeaderTimeout: time.Second, + ReadTimeout: time.Second, + } + s := &httptest.Server{} + s.Config = server + + s.TLS = new(tls.Config) + if s.TLS.NextProtos == nil { + nextProtos := []string{"http/1.1"} + if s.EnableHTTP2 { + nextProtos = []string{"h2"} + } + s.TLS.NextProtos = nextProtos + } + cert, err := tls.X509KeyPair(certPem, key) + require.NoError(t, err) + s.TLS.Certificates = append(s.TLS.Certificates, cert) + suites := tls.CipherSuites() + if len(suitesIds) > 0 { + newSuites := make([]*tls.CipherSuite, 0, len(suitesIds)) + for _, suite := range suites { + for _, id := range suitesIds { + if id == suite.ID { + newSuites = append(newSuites, suite) + } + } + } + suites = newSuites + } + if len(suites) == 0 { + panic("no suites enabled") + } + for _, suite := range suites { + s.TLS.CipherSuites = append(s.TLS.CipherSuites, suite.ID) + } + certpool := x509.NewCertPool() + certificate, err := x509.ParseCertificate(cert.Certificate[0]) + require.NoError(t, err) + certpool.AddCert(certificate) + client := &http.Client{Transport: &http.Transport{}} + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ //nolint:gosec + RootCAs: certpool, + MinVersion: tls.VersionTLS10, + MaxVersion: tls.VersionTLS12, // this so that the ciphersuite work + CipherSuites: suitesIds, + }, + ForceAttemptHTTP2: s.EnableHTTP2, + TLSHandshakeTimeout: time.Second, + ResponseHeaderTimeout: time.Second, + IdleConnTimeout: time.Second, + } + s.Listener, err = net.Listen("tcp", "") + require.NoError(t, err) + s.Listener = tls.NewListener(s.Listener, s.TLS) + return s, client +} diff --git a/js/runner_test.go b/js/runner_test.go index 5e66c8cf6ca..8d3bcc81b45 100644 --- a/js/runner_test.go +++ b/js/runner_test.go @@ -3,14 +3,20 @@ package js import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" "go/build" "io/ioutil" stdlog "log" + "math/big" "net" "net/http" + "net/http/httptest" "net/url" "os" "strings" @@ -846,8 +852,127 @@ func TestVUIntegrationMetrics(t *testing.T) { } } +func GenerateTLSCertificate(t *testing.T, host string, notBefore time.Time, validFor time.Duration) ([]byte, []byte) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + // Only RSA subject keys should have the KeyEncipherment KeyUsage bits set. In + // the context of TLS this KeyUsage is particular to RSA key exchange and + // authentication. + keyUsage |= x509.KeyUsageKeyEncipherment + + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + hosts := strings.Split(host, ",") + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + require.NoError(t, err) + + certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + require.NoError(t, err) + + privBytes, err := x509.MarshalPKCS8PrivateKey(priv) + require.NoError(t, err) + keyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + require.NoError(t, err) + return certPem, keyPem +} + +func GetTestServerWithCertificate(t *testing.T, certPem, key []byte) *httptest.Server { + server := &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + }), + ReadHeaderTimeout: time.Second, + ReadTimeout: time.Second, + } + s := &httptest.Server{} + s.Config = server + + s.TLS = new(tls.Config) + if s.TLS.NextProtos == nil { + nextProtos := []string{"http/1.1"} + if s.EnableHTTP2 { + nextProtos = []string{"h2"} + } + s.TLS.NextProtos = nextProtos + } + cert, err := tls.X509KeyPair(certPem, key) + require.NoError(t, err) + s.TLS.Certificates = append(s.TLS.Certificates, cert) + for _, suite := range tls.CipherSuites() { + if !strings.Contains(suite.Name, "256") { + continue + } + s.TLS.CipherSuites = append(s.TLS.CipherSuites, suite.ID) + } + certpool := x509.NewCertPool() + certificate, err := x509.ParseCertificate(cert.Certificate[0]) + require.NoError(t, err) + certpool.AddCert(certificate) + client := &http.Client{Transport: &http.Transport{}} + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ //nolint:gosec + RootCAs: certpool, + }, + ForceAttemptHTTP2: s.EnableHTTP2, + } + s.Listener, err = net.Listen("tcp", "") + require.NoError(t, err) + s.Listener = tls.NewListener(s.Listener, s.TLS) + s.URL = "https://" + s.Listener.Addr().String() + return s +} + func TestVUIntegrationInsecureRequests(t *testing.T) { t.Parallel() + certPem, keyPem := GenerateTLSCertificate(t, "mybadssl.localhost", time.Now(), 0) + s := GetTestServerWithCertificate(t, certPem, keyPem) + go func() { + _ = s.Config.Serve(s.Listener) + }() + t.Cleanup(func() { + require.NoError(t, s.Config.Close()) + }) + host, port, err := net.SplitHostPort(s.Listener.Addr().String()) + require.NoError(t, err) + ip := net.ParseIP(host) + mybadsslHostname, err := lib.NewHostAddress(ip, port) + require.NoError(t, err) + cert, err := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0]) + require.NoError(t, err) + testdata := map[string]struct { opts lib.Options errMsg string @@ -870,12 +995,15 @@ func TestVUIntegrationInsecureRequests(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() r1, err := getSimpleRunner(t, "/script.js", ` - var http = require("k6/http");; - exports.default = function() { http.get("https://expired.badssl.com/"); } + var http = require("k6/http");; + exports.default = function() { http.get("https://mybadssl.localhost/"); } `) require.NoError(t, err) require.NoError(t, r1.SetOptions(lib.Options{Throw: null.BoolFrom(true)}.Apply(data.opts))) + r1.Bundle.Options.Hosts = map[string]*lib.HostAddress{ + "mybadssl.localhost": mybadsslHostname, + } registry := metrics.NewRegistry() builtinMetrics := metrics.RegisterBuiltinMetrics(registry) r2, err := NewFromArchive( @@ -894,6 +1022,8 @@ func TestVUIntegrationInsecureRequests(t *testing.T) { initVU, err := r.NewVU(1, 1, make(chan metrics.SampleContainer, 100)) require.NoError(t, err) + initVU.(*VU).TLSConfig.RootCAs = x509.NewCertPool() //nolint:forcetypeassert + initVU.(*VU).TLSConfig.RootCAs.AddCert(cert) //nolint:forcetypeassert ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -1137,6 +1267,19 @@ func TestVUIntegrationHosts(t *testing.T) { func TestVUIntegrationTLSConfig(t *testing.T) { t.Parallel() + certPem, keyPem := GenerateTLSCertificate(t, "sha256-badssl.localhost", time.Now(), time.Hour) + s := GetTestServerWithCertificate(t, certPem, keyPem) + go func() { + _ = s.Config.Serve(s.Listener) + }() + t.Cleanup(func() { + require.NoError(t, s.Config.Close()) + }) + host, port, err := net.SplitHostPort(s.Listener.Addr().String()) + require.NoError(t, err) + ip := net.ParseIP(host) + mybadsslHostname, err := lib.NewHostAddress(ip, port) + require.NoError(t, err) unsupportedVersionErrorMsg := "remote error: tls: handshake failure" for _, tag := range build.Default.ReleaseTags { if tag == "go1.12" { @@ -1157,7 +1300,10 @@ func TestVUIntegrationTLSConfig(t *testing.T) { "", }, "UnsupportedCipherSuite": { - lib.Options{TLSCipherSuites: &lib.TLSCipherSuites{tls.TLS_RSA_WITH_RC4_128_SHA}}, + lib.Options{ + TLSCipherSuites: &lib.TLSCipherSuites{tls.TLS_RSA_WITH_RC4_128_SHA}, + TLSVersion: &lib.TLSVersions{Max: tls.VersionTLS12}, + }, "remote error: tls: handshake failure", }, "NullVersion": { @@ -1175,17 +1321,22 @@ func TestVUIntegrationTLSConfig(t *testing.T) { } registry := metrics.NewRegistry() builtinMetrics := metrics.RegisterBuiltinMetrics(registry) + cert, err := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0]) + require.NoError(t, err) for name, data := range testdata { data := data t.Run(name, func(t *testing.T) { t.Parallel() r1, err := getSimpleRunner(t, "/script.js", ` var http = require("k6/http");; - exports.default = function() { http.get("https://sha256.badssl.com/"); } + exports.default = function() { http.get("https://sha256-badssl.localhost/"); } `) require.NoError(t, err) require.NoError(t, r1.SetOptions(lib.Options{Throw: null.BoolFrom(true)}.Apply(data.opts))) + r1.Bundle.Options.Hosts = map[string]*lib.HostAddress{ + "sha256-badssl.localhost": mybadsslHostname, + } r2, err := NewFromArchive( &lib.TestPreInitState{ Logger: testutils.NewLogger(t), @@ -1203,12 +1354,14 @@ func TestVUIntegrationTLSConfig(t *testing.T) { initVU, err := r.NewVU(1, 1, make(chan metrics.SampleContainer, 100)) require.NoError(t, err) + initVU.(*VU).TLSConfig.RootCAs = x509.NewCertPool() //nolint:forcetypeassert + initVU.(*VU).TLSConfig.RootCAs.AddCert(cert) //nolint:forcetypeassert ctx, cancel := context.WithCancel(context.Background()) defer cancel() vu := initVU.Activate(&lib.VUActivationParams{RunContext: ctx}) err = vu.RunOnce() if data.errMsg != "" { - require.Error(t, err) + require.Error(t, err, "for message %q", data.errMsg) assert.Contains(t, err.Error(), data.errMsg) } else { require.NoError(t, err)