diff --git a/README-ZH.md b/README-ZH.md index d265c31..e187fa9 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -138,6 +138,26 @@ Starting Load Test with 1000 requests using 10 concurrent users ``` +示例:指定自定义 ca 证书 +```bash +$ ./cassowary run -u http://localhost:8000 -c 10 -n 1000 --ca /path/to/ca.pem + +Starting Load Test with 1000 requests using 10 concurrent users + +[ omitted for brevity ] + +``` + +示例:指定客户端证书信息 +```bash +$ ./cassowary run -u http://localhost:8000 -c 10 -n 1000 --cert /path/to/client.pem --key /path/to/client-key.pem + +Starting Load Test with 1000 requests using 10 concurrent users + +[ omitted for brevity ] + +``` + **以模块或者library导入Cassowary** diff --git a/README.md b/README.md index 4b1b4e5..92bbeb5 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,30 @@ Starting Load Test with 1000 requests using 10 concurrent users ``` +### Specifying ca certificate +Example specifying ca certificate + +```bash +$ ./cassowary run -u http://localhost:8000 -c 10 -n 1000 --ca /path/to/ca.pem + +Starting Load Test with 1000 requests using 10 concurrent users + +[ omitted for brevity ] + +``` + +### Specifying client authentication certificate +Example specifying client authentication certificate + +```bash +$ ./cassowary run -u http://localhost:8000 -c 10 -n 1000 --cert /path/to/client.pem --key /path/to/client-key.pem + +Starting Load Test with 1000 requests using 10 concurrent users + +[ omitted for brevity ] + +``` + Importing cassowary as a module/library -------- diff --git a/cmd/cassowary/cli.go b/cmd/cassowary/cli.go index cfc7cae..9c44c29 100644 --- a/cmd/cassowary/cli.go +++ b/cmd/cassowary/cli.go @@ -1,9 +1,12 @@ package main import ( + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" + "io/ioutil" "os" "strconv" @@ -151,6 +154,27 @@ func validateCLI(c *cli.Context) error { httpMethod = "GET" } + tlsConfig := new(tls.Config) + if c.String("ca") != "" { + pemCerts, err := ioutil.ReadFile(c.String("ca")) + if err != nil { + return err + } + ca := x509.NewCertPool() + if !ca.AppendCertsFromPEM(pemCerts) { + return fmt.Errorf("failed to read CA from PEM") + } + tlsConfig.RootCAs = ca + } + + if c.String("cert") != "" && c.String("key") != "" { + cert, err := tls.LoadX509KeyPair(c.String("cert"), c.String("key")) + if err != nil { + return err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + cass := &client.Cassowary{ FileMode: false, BaseURL: c.String("url"), @@ -159,6 +183,7 @@ func validateCLI(c *cli.Context) error { RequestHeader: header, Duration: duration, PromExport: prometheusEnabled, + TLSConfig: tlsConfig, PromURL: c.String("prompushgwurl"), Cloudwatch: c.Bool("cloudwatch"), ExportMetrics: c.Bool("json-metrics"), @@ -282,6 +307,18 @@ func runCLI(args []string) { Name: "disable-keep-alive", Usage: "use this flag to disable http keep-alive", }, + cli.StringFlag{ + Name: "ca", + Usage: "ca certificate to verify peer against", + }, + cli.StringFlag{ + Name: "cert", + Usage: "client authentication certificate", + }, + cli.StringFlag{ + Name: "key", + Usage: "client authentication key", + }, }, Action: validateCLIFile, }, @@ -345,6 +382,18 @@ func runCLI(args []string) { Name: "disable-keep-alive", Usage: "use this flag to disable http keep-alive", }, + cli.StringFlag{ + Name: "ca", + Usage: "ca certificate to verify peer against", + }, + cli.StringFlag{ + Name: "cert", + Usage: "client authentication certificate", + }, + cli.StringFlag{ + Name: "key", + Usage: "client authentication key", + }, }, Action: validateCLI, }, diff --git a/docs/LIBRARY.md b/docs/LIBRARY.md index 12a9420..bef9977 100644 --- a/docs/LIBRARY.md +++ b/docs/LIBRARY.md @@ -84,3 +84,65 @@ func main() { fmt.Println(string(jsonMetrics)) } ``` + +**Example 3: Custom TLS config** + +```go +package main + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + + cassowary "github.com/rogerwelin/cassowary/pkg/client" +) + +func main() { + pemCerts, err := ioutil.ReadFile("testdata/ca.pem") + if err != nil { + panic("Invalid ca.pem path") + } + + ca := x509.NewCertPool() + if !ca.AppendCertsFromPEM(pemCerts) { + panic("Failed to read CA from PEM") + } + + cert, err := tls.LoadX509KeyPair("testdata/client.pem", "testdata/client-key.pem") + if err != nil { + panic("Invalid client.pem/client-key.pem path") + } + + clientTLSConfig := &tls.Config{ + RootCAs: ca, + Certificates: []tls.Certificate{cert}, + } + + cass := &cassowary.Cassowary{ + BaseURL: "http://www.example.com", + ConcurrencyLevel: 1, + Requests: 10, + TLSConfig: clientTLSConfig, + DisableTerminalOutput: true, + } + metrics, err := cass.Coordinate() + if err != nil { + panic(err) + } + + // print results + fmt.Printf("%+v\n", metrics) + + // or print as json + jsonMetrics, err := json.Marshal(metrics) + if err != nil { + panic(err) + } + + fmt.Println(string(jsonMetrics)) +} + +``` \ No newline at end of file diff --git a/pkg/client/load.go b/pkg/client/load.go index a4be154..649cd64 100644 --- a/pkg/client/load.go +++ b/pkg/client/load.go @@ -148,6 +148,7 @@ func (c *Cassowary) Coordinate() (ResultMetrics, error) { c.Client = &http.Client{ Timeout: time.Second * time.Duration(c.Timeout), Transport: &http.Transport{ + TLSClientConfig: c.TLSConfig, MaxIdleConnsPerHost: 10000, DisableCompression: false, DisableKeepAlives: c.DisableKeepAlive, diff --git a/pkg/client/load_test.go b/pkg/client/load_test.go index e525fad..94bea3d 100644 --- a/pkg/client/load_test.go +++ b/pkg/client/load_test.go @@ -1,6 +1,9 @@ package client import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" "net/http" "net/http/httptest" "testing" @@ -73,3 +76,66 @@ func TestLoadCoordinateURLPaths(t *testing.T) { t.Errorf("Wanted %d but got %d", 30, len(cass.URLPaths)) } } + +func TestCoordinateTLSConfig(t *testing.T) { + srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("ok")) + })) + + pemCerts, err := ioutil.ReadFile("testdata/ca.pem") + if err != nil { + t.Fatal("Invalid ca.pem path") + } + + ca := x509.NewCertPool() + if !ca.AppendCertsFromPEM(pemCerts) { + t.Fatal("Failed to read CA from PEM") + } + + cert, err := tls.LoadX509KeyPair("testdata/server.pem", "testdata/server-key.pem") + if err != nil { + t.Fatal("Invalid server.pem/server-key.pem path") + } + + srv.TLS = &tls.Config{ + ClientCAs: ca, + ClientAuth: tls.RequireAndVerifyClientCert, + Certificates: []tls.Certificate{cert}, + } + srv.StartTLS() + + cert, err = tls.LoadX509KeyPair("testdata/client.pem", "testdata/client-key.pem") + if err != nil { + t.Fatal("Invalid client.pem/client-key.pem path") + } + clientTLSConfig := &tls.Config{ + RootCAs: ca, + Certificates: []tls.Certificate{cert}, + } + + cass := Cassowary{ + BaseURL: srv.URL, + ConcurrencyLevel: 1, + Requests: 10, + TLSConfig: clientTLSConfig, + DisableTerminalOutput: true, + } + + metrics, err := cass.Coordinate() + if err != nil { + t.Error(err) + } + + if metrics.BaseURL != srv.URL { + t.Errorf("Wanted %s but got %s", srv.URL, metrics.BaseURL) + } + + if metrics.TotalRequests != 10 { + t.Errorf("Wanted %d but got %d", 1, metrics.TotalRequests) + } + + if metrics.FailedRequests != 0 { + t.Errorf("Wanted %d but got %d", 0, metrics.FailedRequests) + } +} diff --git a/pkg/client/testdata/ca-key.pem b/pkg/client/testdata/ca-key.pem new file mode 100644 index 0000000..30f814c --- /dev/null +++ b/pkg/client/testdata/ca-key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCKZiW/gDKygWST6H2EWbSnln/DeaTA9tZzExaPjhGLeluQAY+2k24c +qo4fM4LouV+gBwYFK4EEACKhZANiAAQkLBpmhd9cRaPGV5PXca0MZTb9u98KJusf +NRElfAyKDLrQ8jPEaN7i0uVHeK7gwAGEWsSUhmUC+0YF78jZosaecZiNZ71AgJZ7 +2DKij0x9PLrGfQmSOPtjzxqWbKmgzt8= +-----END EC PRIVATE KEY----- diff --git a/pkg/client/testdata/ca.pem b/pkg/client/testdata/ca.pem new file mode 100644 index 0000000..11dc31a --- /dev/null +++ b/pkg/client/testdata/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICaDCCAe6gAwIBAgIUeox07oT2mMosFiT0SIpi5G+wcngwCgYIKoZIzj0EAwMw +ajELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xEjAQBgNVBAoTCWNhc3Nvd2FyeTEaMBgGA1UECxMRQ2Fzc293 +YXJ5IFRlc3RpbmcwIBcNMjAwNTMxMDUxNDAwWhgPMjEyMDA1MDcwNTE0MDBaMGox +CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4g +RnJhbmNpc2NvMRIwEAYDVQQKEwljYXNzb3dhcnkxGjAYBgNVBAsTEUNhc3Nvd2Fy +eSBUZXN0aW5nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJCwaZoXfXEWjxleT13Gt +DGU2/bvfCibrHzURJXwMigy60PIzxGje4tLlR3iu4MABhFrElIZlAvtGBe/I2aLG +nnGYjWe9QICWe9gyoo9MfTy6xn0Jkjj7Y88almypoM7fo1MwUTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUIccCwpEWbeI0v2vEhPf4 +ONBdr68wDwYDVR0RBAgwBocEfwAAATAKBggqhkjOPQQDAwNoADBlAjEA5xrv47yu +jUHorUWBJVMhDnUyav55W052ho0fNGB4cRgoFAXUX4teVgvLzrnVGRZUAjBaHyhK +ojG8Pg/iJnJNzmCgyJW1dKZTyskifuW28JRyM6LgnOYT5tGT9yH6g10UjvA= +-----END CERTIFICATE----- diff --git a/pkg/client/testdata/ca_csr.json b/pkg/client/testdata/ca_csr.json new file mode 100644 index 0000000..093ff0d --- /dev/null +++ b/pkg/client/testdata/ca_csr.json @@ -0,0 +1,19 @@ +{ + "hosts" : ["127.0.0.1"], + "key": { + "algo": "ecdsa", + "size": 384 + }, + "ca": { + "expiry": "876000h" + }, + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "cassowary", + "OU": "Cassowary Testing", + "ST": "California" + } + ] +} diff --git a/pkg/client/testdata/client-key.pem b/pkg/client/testdata/client-key.pem new file mode 100644 index 0000000..0690e4e --- /dev/null +++ b/pkg/client/testdata/client-key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAc90Blw/NfCnWMXTC4Dbo5+LaA4evCIIfZDbPaI7UVz6UwTBsEFGK/ +/rWIEskJOO+gBwYFK4EEACKhZANiAARTfENUulB4IBZPQFORPmyErND774l+jH4V +JiKPLENCCUKHUDNih+skoincGS2fLjTplrC/ZLACeuvsevr0czcaH0MTkL3aA5FT +9RNbgBMcKKkqC/DtJZ+imr6RwGu3iow= +-----END EC PRIVATE KEY----- diff --git a/pkg/client/testdata/client.pem b/pkg/client/testdata/client.pem new file mode 100644 index 0000000..e7523ee --- /dev/null +++ b/pkg/client/testdata/client.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIUWpA0KuqhONq0Q7xJwp/vXdMdnC4wCgYIKoZIzj0EAwMw +ajELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xEjAQBgNVBAoTCWNhc3Nvd2FyeTEaMBgGA1UECxMRQ2Fzc293 +YXJ5IFRlc3RpbmcwIBcNMjAwNTMxMDUxNDAwWhgPMjEyMDA1MDcwNTE0MDBaMGox +CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4g +RnJhbmNpc2NvMRIwEAYDVQQKEwljYXNzb3dhcnkxGjAYBgNVBAsTEUNhc3Nvd2Fy +eSBUZXN0aW5nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEU3xDVLpQeCAWT0BTkT5s +hKzQ+++Jfox+FSYijyxDQglCh1AzYofrJKIp3Bktny406Zawv2SwAnrr7Hr69HM3 +Gh9DE5C92gORU/UTW4ATHCipKgvw7SWfopq+kcBrt4qMo2UwYzAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E +FgQUu8dTMk2IWBGGnTG6KWvVnPzFEHUwDwYDVR0RBAgwBocEfwAAATAKBggqhkjO +PQQDAwNoADBlAjBfb1Ts88t8kd8nU4/eSVeKRR91VQI/uOIxNVHDuOsyBinx0jgh +eZ+MfZZ/exv319ICMQC0plJI7JSB+lDHbmIEYOiXZUXbONZkbO9ALRRMTqxzh00S +vAju6beNGIFpwQ/q7dg= +-----END CERTIFICATE----- diff --git a/pkg/client/testdata/csr.json b/pkg/client/testdata/csr.json new file mode 100644 index 0000000..16f13cb --- /dev/null +++ b/pkg/client/testdata/csr.json @@ -0,0 +1,16 @@ +{ + "hosts" : ["127.0.0.1"], + "key": { + "algo": "ecdsa", + "size": 384 + }, + "names": [ + { + "C": "US", + "L": "San Francisco", + "O": "cassowary", + "OU": "Cassowary Testing", + "ST": "California" + } + ] +} diff --git a/pkg/client/testdata/gencert.sh b/pkg/client/testdata/gencert.sh new file mode 100755 index 0000000..bc41e6a --- /dev/null +++ b/pkg/client/testdata/gencert.sh @@ -0,0 +1,4 @@ +cfssl gencert -initca ca_csr.json |cfssljson -bare ca +cfssl gencert -ca ca.pem -ca-key ca-key.pem -config signing.json -profile client csr.json |cfssljson -bare client +cfssl gencert -ca ca.pem -ca-key ca-key.pem -config signing.json -profile server csr.json |cfssljson -bare server +rm *.csr diff --git a/pkg/client/testdata/server-key.pem b/pkg/client/testdata/server-key.pem new file mode 100644 index 0000000..a7f6b04 --- /dev/null +++ b/pkg/client/testdata/server-key.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAN6AKzYbaqN9dm5YsDP4qFF0m+jM2lbodOsvmwpYryEhtoq9NuWBKr +s7ycpCBd/N+gBwYFK4EEACKhZANiAAQ1+v02VGuU/Ld5sRQG/vVf9hcAUUA+dqoe +zooZ3Af4yJagE8jCtfG/yXZTmJpYmAKGI8USRdVGG5EL1bS6DULhItaLOVFTlLvC +QJjaZWTI1YGVZgboH0dS+i6XB4O0H70= +-----END EC PRIVATE KEY----- diff --git a/pkg/client/testdata/server.pem b/pkg/client/testdata/server.pem new file mode 100644 index 0000000..fed7e93 --- /dev/null +++ b/pkg/client/testdata/server.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIUJvol07hW1JSVOf3K8XPYEQW/KSAwCgYIKoZIzj0EAwMw +ajELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xEjAQBgNVBAoTCWNhc3Nvd2FyeTEaMBgGA1UECxMRQ2Fzc293 +YXJ5IFRlc3RpbmcwIBcNMjAwNTMxMDUxNDAwWhgPMjEyMDA1MDcwNTE0MDBaMGox +CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4g +RnJhbmNpc2NvMRIwEAYDVQQKEwljYXNzb3dhcnkxGjAYBgNVBAsTEUNhc3Nvd2Fy +eSBUZXN0aW5nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAENfr9NlRrlPy3ebEUBv71 +X/YXAFFAPnaqHs6KGdwH+MiWoBPIwrXxv8l2U5iaWJgChiPFEkXVRhuRC9W0ug1C +4SLWizlRU5S7wkCY2mVkyNWBlWYG6B9HUvoulweDtB+9o2UwYzAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4E +FgQUHGnd67JENhm/66BAP4vQkBuMpS8wDwYDVR0RBAgwBocEfwAAATAKBggqhkjO +PQQDAwNoADBlAjEA1fY2EeJuqZYu08Fr718nUPgcuPUXmaPFA07X5Zabhch5ciUU +4I1izXoLGC3j1x/KAjAzUIqldhF3eGcyuNa9E1k/XO2MAkfoMN2M2DlKSJOFSv6Q +ROesXoL+5ecGGzBv12s= +-----END CERTIFICATE----- diff --git a/pkg/client/testdata/signing.json b/pkg/client/testdata/signing.json new file mode 100644 index 0000000..b66c40e --- /dev/null +++ b/pkg/client/testdata/signing.json @@ -0,0 +1,32 @@ +{ + "signing": { + "default": { + "expiry": "876000h", + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + }, + "profiles": { + "client": { + "expiry": "876000h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + + "server": { + "expiry": "876000h", + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + } + } + } +} diff --git a/pkg/client/types.go b/pkg/client/types.go index 5cc94be..c0e9b2c 100644 --- a/pkg/client/types.go +++ b/pkg/client/types.go @@ -1,6 +1,7 @@ package client import ( + "crypto/tls" "net/http" "github.com/schollz/progressbar" @@ -18,6 +19,7 @@ type Cassowary struct { ExportMetricsFile string PromExport bool Cloudwatch bool + TLSConfig *tls.Config PromURL string RequestHeader []string URLPaths []string