From 419727e1eb1ff1ba84bc3d4fabfbf93e241d270d Mon Sep 17 00:00:00 2001 From: David Eisner Date: Sat, 24 Dec 2016 04:49:29 +0000 Subject: [PATCH] feat(api): Connect to docker behind a name based virtual host proxy (#379) This involves copying and modifying go's httputil.NewSingleHostReverseProxy, which is documented to (perhaps surprisingly) leave the Host header untouched. Instead, we set the Host header to the target host for the connection for the benefit of name based virtual host proxies that make use of this. The value it would otherwise have in this app, typically 'localhost:8000', is strange and unlikely to be any use. See golang/go#7618 and golang/go#10342 --- api/http/docker_handler.go | 45 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/api/http/docker_handler.go b/api/http/docker_handler.go index bb67a3943..f15b4386e 100644 --- a/api/http/docker_handler.go +++ b/api/http/docker_handler.go @@ -11,6 +11,7 @@ import ( "net/http/httputil" "net/url" "os" + "strings" ) // DockerHandler represents an HTTP API handler for proxying requests to the Docker API. @@ -61,14 +62,54 @@ func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration return nil } +// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go +// included here for use in NewSingleHostReverseProxyWithHostHeader +// because its used in NewSingleHostReverseProxy from golang.org/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 +} + +// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy +// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host +// HTTP header, which NewSingleHostReverseProxy deliberately preserves + +func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy { + targetQuery := target.RawQuery + director := func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) + req.Host = req.URL.Host + 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 { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } + } + return &httputil.ReverseProxy{Director: director} +} + func newHTTPProxy(u *url.URL) http.Handler { u.Scheme = "http" - return httputil.NewSingleHostReverseProxy(u) + return NewSingleHostReverseProxyWithHostHeader(u) } func newHTTPSProxy(u *url.URL, endpointConfig *portainer.EndpointConfiguration) (http.Handler, error) { u.Scheme = "https" - proxy := httputil.NewSingleHostReverseProxy(u) + proxy := NewSingleHostReverseProxyWithHostHeader(u) config, err := createTLSConfiguration(endpointConfig.TLSCACertPath, endpointConfig.TLSCertPath, endpointConfig.TLSKeyPath) if err != nil { return nil, err