From 6eacf8ef26eaa12fb0dbe5871470e0daa0588a95 Mon Sep 17 00:00:00 2001 From: Matt Fellows Date: Fri, 16 Aug 2019 22:24:04 +1000 Subject: [PATCH] fix(https): enable external https verifications The local proxy currently assumes that a verification will take place against either a) a local endpoint or b) an http endpoint. It did not support external hosts on https.o It also did not rewrite the Host header correctly (see https://github.com/golang/go/issues/28168) This change: 1. Rewrites the header during proxying 2. Hard codes the local proxy to run only on http - there is no reason why it should run on https even if the endpoint under test _is_. 3. Opens the door for client configured transports during verification, to enable self-signed certificates to be easily used --- dsl/pact.go | 20 ++++++++++------- proxy/http.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/dsl/pact.go b/dsl/pact.go index 6f978754f..bba282349 100644 --- a/dsl/pact.go +++ b/dsl/pact.go @@ -328,9 +328,11 @@ func (p *Pact) VerifyProviderRaw(request types.VerifyRequest) (types.ProviderVer // Configure HTTP Verification Proxy opts := proxy.Options{ - TargetAddress: fmt.Sprintf("%s:%s", u.Hostname(), u.Port()), - TargetScheme: u.Scheme, - Middleware: m, + TargetAddress: fmt.Sprintf("%s:%s", u.Hostname(), u.Port()), + TargetScheme: u.Scheme, + TargetPath: u.Path, + Middleware: m, + InternalRequestPathPrefix: providerStatesSetupPath, } // Starts the message wrapper API with hooks back to the state handlers @@ -342,13 +344,13 @@ func (p *Pact) VerifyProviderRaw(request types.VerifyRequest) (types.ProviderVer // Backwards compatibility, setup old provider states URL if given // Otherwise point to proxy setupURL := request.ProviderStatesSetupURL - if request.ProviderStatesSetupURL == "" { - setupURL = fmt.Sprintf("%s://localhost:%d/__setup", u.Scheme, port) + if request.ProviderStatesSetupURL == "" && len(request.StateHandlers) > 0 { + setupURL = fmt.Sprintf("http://localhost:%d%s", port, providerStatesSetupPath) } // Construct verifier request verificationRequest := types.VerifyRequest{ - ProviderBaseURL: fmt.Sprintf("%s://localhost:%d", u.Scheme, port), // + ProviderBaseURL: fmt.Sprintf("http://localhost:%d", port), PactURLs: request.PactURLs, BrokerURL: request.BrokerURL, Tags: request.Tags, @@ -413,7 +415,7 @@ var checkCliCompatibility = func() { func BeforeEachMiddleware(BeforeEach types.Hook) proxy.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/__setup" { + if r.URL.Path == providerStatesSetupPath { log.Println("[DEBUG] executing before hook") err := BeforeEach() @@ -458,7 +460,7 @@ func AfterEachMiddleware(AfterEach types.Hook) proxy.Middleware { func stateHandlerMiddleware(stateHandlers types.StateHandlers) proxy.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/__setup" { + if r.URL.Path == providerStatesSetupPath { var s *types.ProviderState decoder := json.NewDecoder(r.Body) decoder.Decode(&s) @@ -706,3 +708,5 @@ func (p *Pact) VerifyMessageConsumer(t *testing.T, message *Message, handler Mes return err } + +const providerStatesSetupPath = "/__setup" diff --git a/proxy/http.go b/proxy/http.go index 66db32d0b..fbfc90f1f 100644 --- a/proxy/http.go +++ b/proxy/http.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strings" "github.com/pact-foundation/pact-go/utils" ) @@ -25,12 +26,18 @@ type Options struct { // TargetAddress is the host:port component to proxy TargetAddress string + // TargetPath is the path on the target to proxy + TargetPath string + // ProxyPort is the port to make available for proxying // Defaults to a random port ProxyPort int // Middleware to apply to the Proxy Middleware []Middleware + + // Internal request prefix for proxy to not rewrite + InternalRequestPathPrefix string } // loggingMiddleware logs requests to the proxy @@ -59,13 +66,16 @@ func chainHandlers(mw ...Middleware) Middleware { // HTTPReverseProxy provides a default setup for proxying // internal components within the framework func HTTPReverseProxy(options Options) (int, error) { + log.Println("[DEBUG] starting new proxy with opts", options) port := options.ProxyPort var err error - proxy := httputil.NewSingleHostReverseProxy(&url.URL{ + url := &url.URL{ Scheme: options.TargetScheme, Host: options.TargetAddress, - }) + Path: options.TargetPath, + } + proxy := createProxy(url, options.InternalRequestPathPrefix) if port == 0 { port, err = utils.GetFreePort() @@ -82,3 +92,48 @@ func HTTPReverseProxy(options Options) (int, error) { return port, nil } + +// Adapted from https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go +func createProxy(target *url.URL, ignorePrefix string) *httputil.ReverseProxy { + targetQuery := target.RawQuery + director := func(req *http.Request) { + if !strings.HasPrefix(req.URL.Path, ignorePrefix) { + log.Println("[DEBUG] setting proxy to target") + log.Println("[DEBUG] incoming request", req.URL) + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.Host = target.Host + + req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) + log.Println("[DEBUG] outgoing request", req.URL) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + if _, ok := req.Header["User-Agent"]; !ok { + req.Header.Set("User-Agent", "Pact Go") + } + } else { + log.Println("[DEBUG] setting proxy to internal server") + req.URL.Scheme = "http" + req.URL.Host = "localhost" + req.Host = "localhost" + } + } + return &httputil.ReverseProxy{Director: director} +} + +// From httputil package +// https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +}