Repository: fortio/fortio (go)

Issue:
add option to provide client certs and ca for grpc (done for https runner/curl)
follow up on #138 and #106 

- [x] https (curl and http runner) Done
- [ ] GRPC client: Not done

Pull request:
refactor tlsoptions to common struct and share setup for both http and grpc
fixes #141,#530

Patch:
diff --git a/README.md b/README.md
index 0f45928..806ad09 100644
--- a/README.md
+++ b/README.md
@@ -46,13 +46,13 @@ docker run fortio/fortio load http://www.google.com/ # For a test run
 Or download one of the binary distributions, from the [releases](https://github.com/fortio/fortio/releases) assets page or for instance:
 
 ```shell
-curl -L https://github.com/fortio/fortio/releases/download/v1.23.0/fortio-linux_x64-1.23.0.tgz \
+curl -L https://github.com/fortio/fortio/releases/download/v1.24.0/fortio-linux_x64-1.24.0.tgz \
  | sudo tar -C / -xvzpf -
 # or the debian package
-wget https://github.com/fortio/fortio/releases/download/v1.23.0/fortio_1.23.0_amd64.deb
-dpkg -i fortio_1.23.0_amd64.deb
+wget https://github.com/fortio/fortio/releases/download/v1.24.0/fortio_1.24.0_amd64.deb
+dpkg -i fortio_1.24.0_amd64.deb
 # or the rpm
-rpm -i https://github.com/fortio/fortio/releases/download/v1.23.0/fortio-1.23.0-1.x86_64.rpm
+rpm -i https://github.com/fortio/fortio/releases/download/v1.24.0/fortio-1.24.0-1.x86_64.rpm
 ```
 
 On a MacOS you can also install Fortio using [Homebrew](https://brew.sh/):
@@ -61,7 +61,7 @@ On a MacOS you can also install Fortio using [Homebrew](https://brew.sh/):
 brew install fortio
 ```
 
-On Windows, download https://github.com/fortio/fortio/releases/download/v1.23.0/fortio_win_1.23.0.zip and extract `fortio.exe` to any location, then using the Windows Command Prompt:
+On Windows, download https://github.com/fortio/fortio/releases/download/v1.24.0/fortio_win_1.24.0.zip and extract `fortio.exe` to any location, then using the Windows Command Prompt:
 ```
 fortio.exe server
 ```
@@ -106,7 +106,7 @@ Full list of command line flags (`fortio help`):
 <details>
 <!-- use release/updateFlags.sh to update this section -->
 <pre>
-Φορτίο 1.23.0 usage:
+Φορτίο 1.24.0 usage:
 where command is one of: load (load testing), server (starts ui, http-echo,
  redirect, proxies, tcp-echo and grpc ping servers), tcp-echo (only the tcp-echo
  server), report (report only UI server), redirect (only the redirect server),
@@ -204,7 +204,7 @@ kbytes (default 128)
   -json path
         Json output to provided file path or '-' for stdout (empty = no json
 output, unless -a is used)
-  -k    Do not verify certs in https connections
+  -k    Do not verify certs in https/tls/grpc connections
   -keepalive
         Keep connection alive (only for fast http 1.1) (default true)
   -key Path
diff --git a/bincommon/commonflags.go b/bincommon/commonflags.go
index 9161fa2..bad5400 100644
--- a/bincommon/commonflags.go
+++ b/bincommon/commonflags.go
@@ -64,7 +64,7 @@ var (
 	httpReqTimeoutFlag  = flag.Duration("timeout", fhttp.HTTPReqTimeOutDefaultValue, "Connection and read timeout value (for http)")
 	stdClientFlag       = flag.Bool("stdclient", false, "Use the slower net/http standard client (slower but supports h2)")
 	http10Flag          = flag.Bool("http1.0", false, "Use http1.0 (instead of http 1.1)")
-	httpsInsecureFlag   = flag.Bool("k", false, "Do not verify certs in https connections")
+	httpsInsecureFlag   = flag.Bool("k", false, "Do not verify certs in https/tls/grpc connections")
 	httpsInsecureFlagL  = flag.Bool("https-insecure", false, "Long form of the -k flag")
 	resolve             = flag.String("resolve", "", "Resolve host name to this `IP`")
 	headersFlags        headersFlagList
diff --git a/fgrpc/grpcrunner.go b/fgrpc/grpcrunner.go
index 68a58a3..5b6951f 100644
--- a/fgrpc/grpcrunner.go
+++ b/fgrpc/grpcrunner.go
@@ -16,7 +16,6 @@ package fgrpc // import "fortio.org/fortio/fgrpc"
 
 import (
 	"context"
-	"crypto/tls"
 	"fmt"
 	"net"
 	"os"
@@ -25,6 +24,7 @@ import (
 	"strings"
 	"time"
 
+	"fortio.org/fortio/fhttp"
 	"fortio.org/fortio/fnet"
 	"fortio.org/fortio/log"
 	"fortio.org/fortio/periodic"
@@ -38,20 +38,14 @@ import (
 // it will override the virtual host name of authority in requests.
 func Dial(o *GRPCRunnerOptions) (conn *grpc.ClientConn, err error) {
 	var opts []grpc.DialOption
-	switch {
-	case o.CACert != "":
-		var creds credentials.TransportCredentials
-		creds, err = credentials.NewClientTLSFromFile(o.CACert, o.CertOverride)
+	if o.CACert != "" || strings.HasPrefix(o.Destination, fnet.PrefixHTTPS) {
+		tlsConfig, err := o.TLSOptions.TLSClientConfig()
 		if err != nil {
-			log.Errf("Invalid TLS credentials: %v\n", err)
 			return nil, err
 		}
-		log.Infof("Using CA certificate %v to construct TLS credentials", o.CACert)
-		opts = append(opts, grpc.WithTransportCredentials(creds))
-	case strings.HasPrefix(o.Destination, fnet.PrefixHTTPS):
-		creds := credentials.NewTLS(&tls.Config{InsecureSkipVerify: o.Insecure}) // nolint: gosec // explicit flag
-		opts = append(opts, grpc.WithTransportCredentials(creds))
-	default:
+		tlsConfig.ServerName = o.CertOverride
+		opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
+	} else {
 		opts = append(opts, grpc.WithInsecure())
 	}
 	serverAddr := grpcDestination(o.Destination)
@@ -114,18 +108,16 @@ func (grpcstate *GRPCRunnerResults) Run(t int) {
 // options.
 type GRPCRunnerOptions struct {
 	periodic.RunnerOptions
+	fhttp.TLSOptions
 	Destination        string
 	Service            string        // Service to be checked when using grpc health check
 	Profiler           string        // file to save profiles to. defaults to no profiling
 	Payload            string        // Payload to be sent for grpc ping service
 	Streams            int           // number of streams. total go routines and data streams will be streams*numthreads.
 	Delay              time.Duration // Delay to be sent when using grpc ping service
-	CACert             string        // Path to CA certificate for grpc TLS
 	CertOverride       string        // Override the cert virtual host of authority for testing
-	Insecure           bool          // Allow unknown CA / self signed
 	AllowInitialErrors bool          // whether initial errors don't cause an abort
 	UsePing            bool          // use our own Ping proto for grpc load instead of standard health check one.
-	UnixDomainSocket   string        // unix domain socket path to use for physical connection instead of Destination
 }
 
 // RunGRPCTest runs an http test and returns the aggregated stats.
diff --git a/fgrpc/pingsrv.go b/fgrpc/pingsrv.go
index 2ac410f..cff169f 100644
--- a/fgrpc/pingsrv.go
+++ b/fgrpc/pingsrv.go
@@ -21,6 +21,7 @@ import (
 	"os"
 	"time"
 
+	"fortio.org/fortio/fhttp"
 	"fortio.org/fortio/fnet"
 	"fortio.org/fortio/log"
 	"fortio.org/fortio/stats"
@@ -105,8 +106,8 @@ func PingServerTCP(port, cert, key, healthServiceName string, maxConcurrentStrea
 
 // PingClientCall calls the ping service (presumably running as PingServer on
 // the destination). returns the average round trip in seconds.
-func PingClientCall(serverAddr, cacert string, n int, payload string, delay time.Duration, insecure bool) (float64, error) {
-	o := GRPCRunnerOptions{Destination: serverAddr, CACert: cacert, Insecure: insecure}
+func PingClientCall(serverAddr string, n int, payload string, delay time.Duration, tlsOpts *fhttp.TLSOptions) (float64, error) {
+	o := GRPCRunnerOptions{Destination: serverAddr, TLSOptions: *tlsOpts}
 	conn, err := Dial(&o) // somehow this never seem to error out, error comes later
 	if err != nil {
 		return -1, err // error already logged
@@ -163,9 +164,9 @@ type HealthResultMap map[string]int64
 
 // GrpcHealthCheck makes a grpc client call to the standard grpc health check
 // service.
-func GrpcHealthCheck(serverAddr, cacert string, svcname string, n int, insecure bool) (*HealthResultMap, error) {
+func GrpcHealthCheck(serverAddr, svcname string, n int, tlsOpts *fhttp.TLSOptions) (*HealthResultMap, error) {
 	log.Debugf("GrpcHealthCheck for %s svc '%s', %d iterations", serverAddr, svcname, n)
-	o := GRPCRunnerOptions{Destination: serverAddr, CACert: cacert, Insecure: insecure}
+	o := GRPCRunnerOptions{Destination: serverAddr, TLSOptions: *tlsOpts}
 	conn, err := Dial(&o)
 	if err != nil {
 		return nil, err
diff --git a/fhttp/http_client.go b/fhttp/http_client.go
index 2fdd4f8..37348a7 100644
--- a/fhttp/http_client.go
+++ b/fhttp/http_client.go
@@ -19,7 +19,6 @@ import (
 	"bytes"
 	"context"
 	"crypto/tls"
-	"crypto/x509"
 	"errors"
 	"fmt"
 	"io"
@@ -160,6 +159,7 @@ const (
 
 // HTTPOptions holds the common options of both http clients and the headers.
 type HTTPOptions struct {
+	TLSOptions
 	URL               string
 	NumConnections    int  // num connections (for std client)
 	Compression       bool // defaults to no compression, only used by std client
@@ -167,28 +167,21 @@ type HTTPOptions struct {
 	HTTP10            bool // defaults to http1.1
 	DisableKeepAlive  bool // so default is keep alive
 	AllowHalfClose    bool // if not keepalive, whether to half close after request
-	Insecure          bool // do not verify certs for https
 	FollowRedirects   bool // For the Std Client only: follow redirects.
 	initDone          bool
 	https             bool   // whether URLSchemeCheck determined this was an https:// call or not
-	CACert            string // `Path` to a custom CA certificate file to be used
-	Cert              string // `Path` to the certificate file to be used
-	Key               string // `Path` to the key file used
 	Resolve           string // resolve Common Name to this ip when use CN as target url
 	// ExtraHeaders to be added to each request (UserAgent and headers set through AddAndValidateExtraHeader()).
 	extraHeaders http.Header
 	// Host is treated specially, remember that virtual header separately.
-	hostOverride   string
-	HTTPReqTimeOut time.Duration // timeout value for http request
-
-	UserCredentials string // user credentials for authorization
-	ContentType     string // indicates request body type, implies POST instead of GET
-	Payload         []byte // body for http request, implies POST if not empty.
-
-	UnixDomainSocket string // Path of unix domain socket to use instead of host:port from URL
-	LogErrors        bool   // whether to log non 2xx code as they occur or not
-	ID               int    // id to use for logging (thread id when used as a runner)
-	SequentialWarmup bool   // whether to do http(s) runs warmup sequentially or in parallel (new default is //)
+	hostOverride     string
+	HTTPReqTimeOut   time.Duration // timeout value for http request
+	UserCredentials  string        // user credentials for authorization
+	ContentType      string        // indicates request body type, implies POST instead of GET
+	Payload          []byte        // body for http request, implies POST if not empty.
+	LogErrors        bool          // whether to log non 2xx code as they occur or not
+	ID               int           // id to use for logging (thread id when used as a runner)
+	SequentialWarmup bool          // whether to do http(s) runs warmup sequentially or in parallel (new default is //)
 }
 
 // ResetHeaders resets all the headers, including the User-Agent: one (and the Host: logical special header).
@@ -439,11 +432,12 @@ func NewStdClient(o *HTTPOptions) (*Client, error) {
 		},
 		TLSHandshakeTimeout: o.HTTPReqTimeOut,
 	}
-	tr.TLSClientConfig, err = o.TLSClientConfig()
-	if err != nil {
-		return nil, err
+	if o.https {
+		tr.TLSClientConfig, err = o.TLSOptions.TLSClientConfig()
+		if err != nil {
+			return nil, err
+		}
 	}
-
 	client := Client{
 		url:                  o.URL,
 		path:                 req.URL.Path,
@@ -470,43 +464,6 @@ func NewStdClient(o *HTTPOptions) (*Client, error) {
 	return &client, nil
 }
 
-// TLSClientConfig creates a tls.Config based on input HTTPOptions.
-// ServerName is set later (once host is determined after URL parsing
-// and depending on hostOverride).
-func (h *HTTPOptions) TLSClientConfig() (*tls.Config, error) {
-	if !h.https {
-		return nil, nil
-	}
-	var res *tls.Config
-
-	res = &tls.Config{MinVersion: tls.VersionTLS12}
-	if h.Insecure {
-		log.LogVf("Using insecure https")
-		res.InsecureSkipVerify = true
-	}
-	if len(h.Cert) > 0 && len(h.Key) > 0 {
-		cert, err := tls.LoadX509KeyPair(h.Cert, h.Key)
-		if err != nil {
-			log.Errf("LoadX509KeyPair error for cert %v / key %v: %v", h.Cert, h.Key, err)
-			return nil, err
-		}
-		res.Certificates = []tls.Certificate{cert}
-	}
-	if len(h.CACert) > 0 {
-		// Load CA cert
-		caCert, err := ioutil.ReadFile(h.CACert)
-		if err != nil {
-			log.Errf("Unable to read CA from %v: %v", h.CACert, err)
-			return nil, err
-		}
-		log.LogVf("Using custom CA from %v", h.CACert)
-		caCertPool := x509.NewCertPool()
-		caCertPool.AppendCertsFromPEM(caCert)
-		res.RootCAs = caCertPool
-	}
-	return res, nil
-}
-
 // FetchURL fetches the data at the given url using the standard client and default options.
 // Returns the http status code (http.StatusOK == 200 for success) and the data.
 // To be used only for single fetches or when performance doesn't matter as the client is closed at the end.
@@ -599,15 +556,17 @@ func NewFastClient(o *HTTPOptions) (Fetcher, error) {
 		log.Errf("Bad url '%s' : %v", urlString, err)
 		return nil, err
 	}
-	tlsConfig, err := o.TLSClientConfig()
-	if err != nil {
-		return nil, err
-	}
 	// note: Host includes the port
 	bc := FastClient{
 		url: o.URL, host: url.Host, hostname: url.Hostname(), port: url.Port(),
 		http10: o.HTTP10, halfClose: o.AllowHalfClose, logErrors: o.LogErrors, id: o.ID,
-		https: o.https, tlsConfig: tlsConfig,
+		https: o.https,
+	}
+	if o.https {
+		bc.tlsConfig, err = o.TLSOptions.TLSClientConfig()
+		if err != nil {
+			return nil, err
+		}
 	}
 	bc.buffer = make([]byte, BufferSizeKb*1024)
 	if bc.port == "" {
@@ -640,8 +599,8 @@ func NewFastClient(o *HTTPOptions) (Fetcher, error) {
 	if customHostHeader {
 		host = o.hostOverride
 	}
-	if tlsConfig != nil {
-		tlsConfig.ServerName = host
+	if bc.tlsConfig != nil {
+		bc.tlsConfig.ServerName = host
 	}
 	var buf bytes.Buffer
 	buf.WriteString(method + " " + url.RequestURI() + " HTTP/" + proto + "\r\n")
diff --git a/fhttp/http_utils.go b/fhttp/http_utils.go
index 6f2d1da..bb0b349 100644
--- a/fhttp/http_utils.go
+++ b/fhttp/http_utils.go
@@ -15,10 +15,13 @@
 package fhttp // import "fortio.org/fortio/fhttp"
 
 import (
+	"crypto/tls"
+	"crypto/x509"
 	"encoding/base64"
 	"flag"
 	"html/template"
 	"io"
+	"io/ioutil"
 	"math/rand"
 	"net/http"
 	"strconv"
@@ -33,6 +36,49 @@ import (
 	"fortio.org/fortio/stats"
 )
 
+// TLSOptions are common TLS related options between https and grpc.
+type TLSOptions struct {
+	Insecure         bool   // Do not verify certs
+	CACert           string // `Path` to a custom CA certificate file to be used
+	Cert             string // `Path` to the certificate file to be used
+	Key              string // `Path` to the key file used
+	UnixDomainSocket string // `Path`` of unix domain socket to use instead of host:port
+}
+
+// TLSClientConfig creates a tls.Config based on input TLSOptions.
+// For https, ServerName is set later (once host is determined after URL parsing
+// and depending on hostOverride).
+func (to *TLSOptions) TLSClientConfig() (*tls.Config, error) {
+	var res *tls.Config
+
+	res = &tls.Config{MinVersion: tls.VersionTLS12}
+	if to.Insecure {
+		log.LogVf("Using insecure https")
+		res.InsecureSkipVerify = true
+	}
+	if len(to.Cert) > 0 && len(to.Key) > 0 {
+		cert, err := tls.LoadX509KeyPair(to.Cert, to.Key)
+		if err != nil {
+			log.Errf("LoadX509KeyPair error for cert %v / key %v: %v", to.Cert, to.Key, err)
+			return nil, err
+		}
+		res.Certificates = []tls.Certificate{cert}
+	}
+	if len(to.CACert) > 0 {
+		// Load CA cert
+		caCert, err := ioutil.ReadFile(to.CACert)
+		if err != nil {
+			log.Errf("Unable to read CA from %v: %v", to.CACert, err)
+			return nil, err
+		}
+		log.LogVf("Using custom CA from %v", to.CACert)
+		caCertPool := x509.NewCertPool()
+		caCertPool.AppendCertsFromPEM(caCert)
+		res.RootCAs = caCertPool
+	}
+	return res, nil
+}
+
 // Used for the fast case insensitive search.
 const toUpperMask = ^byte('a' - 'A')
 
diff --git a/fortio_main.go b/fortio_main.go
index 6cbef74..7d7ca49 100644
--- a/fortio_main.go
+++ b/fortio_main.go
@@ -420,16 +420,14 @@ func fortioLoad(justCurl bool, percList []float64) {
 		o := fgrpc.GRPCRunnerOptions{
 			RunnerOptions:      ro,
 			Destination:        url,
-			CACert:             *bincommon.CACertFlag,
-			Insecure:           bincommon.TLSInsecure(),
 			Service:            *healthSvcFlag,
 			Streams:            *streamsFlag,
 			AllowInitialErrors: *allowInitialErrorsFlag,
 			Payload:            httpOpts.PayloadString(),
 			Delay:              *pingDelayFlag,
 			UsePing:            *doPingLoadFlag,
-			UnixDomainSocket:   httpOpts.UnixDomainSocket,
 		}
+		o.TLSOptions = httpOpts.TLSOptions
 		res, err = fgrpc.RunGRPCTest(&o)
 	} else if strings.HasPrefix(url, tcprunner.TCPURLPrefix) {
 		o := tcprunner.RunnerOptions{
@@ -514,13 +512,12 @@ func grpcClient() {
 	if count <= 0 {
 		count = 1
 	}
-	cert := *bincommon.CACertFlag
+	httpOpts := bincommon.SharedHTTPOptions()
 	var err error
 	if *doHealthFlag {
-		_, err = fgrpc.GrpcHealthCheck(host, cert, *healthSvcFlag, count, bincommon.TLSInsecure())
+		_, err = fgrpc.GrpcHealthCheck(host, *healthSvcFlag, count, &httpOpts.TLSOptions)
 	} else {
-		httpOpts := bincommon.SharedHTTPOptions()
-		_, err = fgrpc.PingClientCall(host, cert, count, httpOpts.PayloadString(), *pingDelayFlag, httpOpts.Insecure)
+		_, err = fgrpc.PingClientCall(host, count, httpOpts.PayloadString(), *pingDelayFlag, &httpOpts.TLSOptions)
 	}
 	if err != nil {
 		// already logged
diff --git a/ui/restHandler.go b/ui/restHandler.go
index 116daa7..814f15f 100644
--- a/ui/restHandler.go
+++ b/ui/restHandler.go
@@ -247,8 +247,8 @@ func Run(w http.ResponseWriter, r *http.Request, jd map[string]interface{},
 			Destination:   url,
 			UsePing:       grpcPing,
 			Delay:         grpcPingDelay,
-			Insecure:      httpopts.Insecure,
 		}
+		o.TLSOptions = httpopts.TLSOptions
 		if grpcSecure {
 			o.Destination = fhttp.AddHTTPS(url)
 		}
diff --git a/ui/uihandler.go b/ui/uihandler.go
index 9718768..fc8d87e 100644
--- a/ui/uihandler.go
+++ b/ui/uihandler.go
@@ -287,8 +287,8 @@ func Handler(w http.ResponseWriter, r *http.Request) {
 				Destination:   url,
 				UsePing:       grpcPing,
 				Delay:         grpcPingDelay,
-				Insecure:      httpsInsecure,
 			}
+			o.TLSOptions = httpopts.TLSOptions
 			if grpcSecure {
 				o.Destination = fhttp.AddHTTPS(url)
 			}


Test patch:
diff --git a/fgrpc/grpcrunner_test.go b/fgrpc/grpcrunner_test.go
index 36b4978..a0626f3 100644
--- a/fgrpc/grpcrunner_test.go
+++ b/fgrpc/grpcrunner_test.go
@@ -20,6 +20,7 @@ import (
 	"testing"
 	"time"
 
+	"fortio.org/fortio/fhttp"
 	"fortio.org/fortio/fnet"
 	"fortio.org/fortio/log"
 	"fortio.org/fortio/periodic"
@@ -68,15 +69,15 @@ func TestGRPCRunner(t *testing.T) {
 			name: "valid secure runner",
 			runnerOpts: GRPCRunnerOptions{
 				Destination: sDest,
-				CACert:      caCrt,
+				TLSOptions:  fhttp.TLSOptions{CACert: caCrt},
 			},
 			expect: true,
 		},
 		{
 			name: "valid unix domain socket runner",
 			runnerOpts: GRPCRunnerOptions{
-				Destination:      uDest,
-				UnixDomainSocket: uPath.String(),
+				Destination: uDest,
+				TLSOptions:  fhttp.TLSOptions{UnixDomainSocket: uPath.String()},
 			},
 			expect: true,
 		},
@@ -129,7 +130,7 @@ func TestGRPCRunner(t *testing.T) {
 			name: "invalid name in secure runner cert",
 			runnerOpts: GRPCRunnerOptions{
 				Destination:  sDest,
-				CACert:       caCrt,
+				TLSOptions:   fhttp.TLSOptions{CACert: caCrt},
 				CertOverride: "invalidName",
 			},
 			expect: false,
@@ -138,7 +139,7 @@ func TestGRPCRunner(t *testing.T) {
 			name: "invalid cert for secure runner",
 			runnerOpts: GRPCRunnerOptions{
 				Destination: sDest,
-				CACert:      "../missing/cert.crt",
+				TLSOptions:  fhttp.TLSOptions{CACert: "../missing/cert.crt"},
 			},
 			expect: false,
 		},
@@ -240,7 +241,7 @@ func TestGRPCRunnerWithError(t *testing.T) {
 			name: "secure runner",
 			runnerOpts: GRPCRunnerOptions{
 				Destination: sDest,
-				CACert:      caCrt,
+				TLSOptions:  fhttp.TLSOptions{CACert: caCrt},
 			},
 		},
 		{
@@ -253,14 +254,14 @@ func TestGRPCRunnerWithError(t *testing.T) {
 			name: "invalid secure runner to insecure server",
 			runnerOpts: GRPCRunnerOptions{
 				Destination: iDest,
-				CACert:      caCrt,
+				TLSOptions:  fhttp.TLSOptions{CACert: caCrt},
 			},
 		},
 		{
 			name: "invalid name in runner cert",
 			runnerOpts: GRPCRunnerOptions{
 				Destination:  sDest,
-				CACert:       caCrt,
+				TLSOptions:   fhttp.TLSOptions{CACert: caCrt},
 				CertOverride: "invalidName",
 			},
 		},
diff --git a/fgrpc/pingsrv_test.go b/fgrpc/pingsrv_test.go
index 0ce2818..457c552 100644
--- a/fgrpc/pingsrv_test.go
+++ b/fgrpc/pingsrv_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 	"time"
 
+	"fortio.org/fortio/fhttp"
 	"fortio.org/fortio/log"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/health/grpc_health_v1"
@@ -31,7 +32,10 @@ func init() {
 }
 
 func TestPingServer(t *testing.T) {
-	TLSInsecure := false
+	TLSSecure := &fhttp.TLSOptions{CACert: caCrt, Insecure: false}
+	TLSSecureMissingCert := &fhttp.TLSOptions{Insecure: false}
+	TLSSecureBadCert := &fhttp.TLSOptions{CACert: failCrt, Insecure: true}
+	TLSInsecure := &fhttp.TLSOptions{Insecure: true}
 	iPort := PingServerTCP("0", "", "", "foo", 0)
 	iAddr := fmt.Sprintf("localhost:%d", iPort)
 	t.Logf("insecure grpc ping server running, will connect to %s", iAddr)
@@ -39,7 +43,7 @@ func TestPingServer(t *testing.T) {
 	sAddr := fmt.Sprintf("localhost:%d", sPort)
 	t.Logf("secure grpc ping server running, will connect to %s", sAddr)
 	delay := 100 * time.Millisecond
-	latency, err := PingClientCall(iAddr, "", 7, "test payload", delay, TLSInsecure)
+	latency, err := PingClientCall(iAddr, 7, "test payload", delay, TLSInsecure)
 	if err != nil || latency < delay.Seconds() || latency > 10.*delay.Seconds() {
 		t.Errorf("Unexpected result %f, %v with ping calls and delay of %v", latency, err, delay)
 	}
@@ -49,29 +53,29 @@ func TestPingServer(t *testing.T) {
 		t.Errorf("Unexpected result %f, %v with ping calls", latency, err)
 	}
 	*/
-	if latency, err := PingClientCall(sAddr, caCrt, 7, "test payload", 0, TLSInsecure); err != nil || latency <= 0 {
+	if latency, err := PingClientCall(sAddr, 7, "test payload", 0, TLSSecure); err != nil || latency <= 0 {
 		t.Errorf("Unexpected result %f, %v with ping calls", latency, err)
 	}
-	if latency, err := PingClientCall(iAddr, caCrt, 1, "", 0, TLSInsecure); err == nil {
+	if latency, err := PingClientCall(iAddr, 1, "", 0, TLSSecure); err == nil {
 		t.Errorf("Should have had an error instead of result %f for secure ping to insecure port", latency)
 	}
-	if _, err := PingClientCall("https://"+sAddr, "", 1, "", 0, true); err != nil {
+	if _, err := PingClientCall("https://"+sAddr, 1, "", 0, TLSInsecure); err != nil {
 		t.Errorf("Should have had no error for secure with bad cert and insecure flag: %v", err)
 	}
-	if latency, err := PingClientCall("https://"+sAddr, "", 1, "", 0, false); err == nil {
+	if latency, err := PingClientCall("https://"+sAddr, 1, "", 0, TLSSecureMissingCert); err == nil {
 		t.Errorf("Should have had error for secure with bad cert and no insecure flag: %v", latency)
 	}
-	if latency, err := PingClientCall(sAddr, "", 1, "", 0, TLSInsecure); err == nil {
+	if latency, err := PingClientCall(sAddr, 1, "", 0, TLSInsecure); err == nil {
 		t.Errorf("Should have had an error instead of result %f for insecure ping to secure port", latency)
 	}
 	if creds, err := credentials.NewServerTLSFromFile(failCrt, failKey); err == nil {
 		t.Errorf("Should have had an error instead of result %f for ping server", creds)
 	}
 	serving := grpc_health_v1.HealthCheckResponse_SERVING.String()
-	if r, err := GrpcHealthCheck(iAddr, "", "", 1, TLSInsecure); err != nil || (*r)[serving] != 1 {
+	if r, err := GrpcHealthCheck(iAddr, "", 1, TLSInsecure); err != nil || (*r)[serving] != 1 {
 		t.Errorf("Unexpected result %+v, %v with empty service health check", r, err)
 	}
-	if r, err := GrpcHealthCheck(sAddr, caCrt, "", 1, TLSInsecure); err != nil || (*r)[serving] != 1 {
+	if r, err := GrpcHealthCheck(sAddr, "", 1, TLSSecure); err != nil || (*r)[serving] != 1 {
 		t.Errorf("Unexpected result %+v, %v with empty service health check", r, err)
 	}
 	/* re-enable once we get https://demo.fortio.org/
@@ -79,19 +83,19 @@ func TestPingServer(t *testing.T) {
 		t.Errorf("Unexpected result %+v, %v with empty service health check", r, err)
 	}
 	*/
-	if r, err := GrpcHealthCheck(iAddr, "", "foo", 3, TLSInsecure); err != nil || (*r)[serving] != 3 {
+	if r, err := GrpcHealthCheck(iAddr, "foo", 3, TLSInsecure); err != nil || (*r)[serving] != 3 {
 		t.Errorf("Unexpected result %+v, %v with health check for same service as started (foo)", r, err)
 	}
-	if r, err := GrpcHealthCheck(sAddr, caCrt, "foo", 3, TLSInsecure); err != nil || (*r)[serving] != 3 {
+	if r, err := GrpcHealthCheck(sAddr, "foo", 3, TLSSecure); err != nil || (*r)[serving] != 3 {
 		t.Errorf("Unexpected result %+v, %v with health check for same service as started (foo)", r, err)
 	}
-	if r, err := GrpcHealthCheck(iAddr, "", "willfail", 1, TLSInsecure); err == nil || r != nil {
+	if r, err := GrpcHealthCheck(iAddr, "willfail", 1, TLSInsecure); err == nil || r != nil {
 		t.Errorf("Was expecting error when using unknown service, didn't get one, got %+v", r)
 	}
-	if r, err := GrpcHealthCheck(sAddr, caCrt, "willfail", 1, TLSInsecure); err == nil || r != nil {
+	if r, err := GrpcHealthCheck(sAddr, "willfail", 1, TLSSecure); err == nil || r != nil {
 		t.Errorf("Was expecting error when using unknown service, didn't get one, got %+v", r)
 	}
-	if r, err := GrpcHealthCheck(sAddr, failCrt, "willfail", 1, TLSInsecure); err == nil {
+	if r, err := GrpcHealthCheck(sAddr, "willfail", 1, TLSSecureBadCert); err == nil {
 		t.Errorf("Was expecting dial error when using invalid certificate, didn't get one, got %+v", r)
 	}
 	// 2nd server on same port should fail to bind:
diff --git a/fhttp/http_test.go b/fhttp/http_test.go
index b0641ac..88687ec 100644
--- a/fhttp/http_test.go
+++ b/fhttp/http_test.go
@@ -579,7 +579,7 @@ func TestUnixDomainHttp(t *testing.T) {
 	if addr == nil {
 		t.Fatalf("Error for Serve for %s", uds)
 	}
-	o := HTTPOptions{UnixDomainSocket: uds, URL: "http://foo.bar:123/debug1"}
+	o := HTTPOptions{TLSOptions: TLSOptions{UnixDomainSocket: uds}, URL: "http://foo.bar:123/debug1"}
 	client, _ := NewClient(&o)
 	code, data, _ := client.Fetch()
 	if code != http.StatusOK {
@@ -1415,7 +1415,7 @@ func TestInsecureRequest(t *testing.T) {
 		o := HTTPOptions{
 			DisableFastClient: tst.fastClient,
 			URL:               expiredURL,
-			Insecure:          tst.insecure,
+			TLSOptions:        TLSOptions{Insecure: tst.insecure},
 		}
 		code, _ := Fetch(&o)
 		if code != tst.code {
@@ -1446,7 +1446,7 @@ func TestInsecureRequestWithResolve(t *testing.T) {
 		o := HTTPOptions{
 			DisableFastClient: tst.fastClient,
 			URL:               url,
-			Insecure:          tst.insecure,
+			TLSOptions:        TLSOptions{Insecure: tst.insecure},
 			Resolve:           "127.0.0.1",
 		}
 		code, _ := Fetch(&o)