diff --git a/components/ws-proxy/pkg/proxy/config.go b/components/ws-proxy/pkg/proxy/config.go index 85ee8e894378c2..f0e2c57fc89c6e 100644 --- a/components/ws-proxy/pkg/proxy/config.go +++ b/components/ws-proxy/pkg/proxy/config.go @@ -120,8 +120,9 @@ func (c *GitpodInstallation) Validate() error { // BlobServerConfig configures where to serve the IDE from. type BlobServerConfig struct { - Scheme string `json:"scheme"` - Host string `json:"host"` + Scheme string `json:"scheme"` + Host string `json:"host"` + PathPrefix string `json:"pathPrefix"` } // Validate validates the configuration to catch issues during startup and not at runtime. diff --git a/components/ws-proxy/pkg/proxy/pass.go b/components/ws-proxy/pkg/proxy/pass.go index 5c1a1d0eb1d466..aa7208b479da43 100644 --- a/components/ws-proxy/pkg/proxy/pass.go +++ b/components/ws-proxy/pkg/proxy/pass.go @@ -44,6 +44,59 @@ type targetResolver func(*Config, WorkspaceInfoProvider, *http.Request) (*url.UR type responseHandler func(*http.Response, *http.Request) error +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 +} + +func joinURLPath(a, b *url.URL) (path, rawpath string) { + if a.RawPath == "" && b.RawPath == "" { + return singleJoiningSlash(a.Path, b.Path), "" + } + // Same as singleJoiningSlash, but uses EscapedPath to determine + // whether a slash should be added + apath := a.EscapedPath() + bpath := b.EscapedPath() + + aslash := strings.HasSuffix(apath, "/") + bslash := strings.HasPrefix(bpath, "/") + + switch { + case aslash && bslash: + return a.Path + b.Path[1:], apath + bpath[1:] + case !aslash && !bslash: + return a.Path + "/" + b.Path, apath + "/" + bpath + } + return a.Path + b.Path, apath + bpath +} + +func NewSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy { + targetQuery := target.RawQuery + director := func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.Host = target.Host + req.URL.Path, req.URL.RawPath = joinURLPath(target, 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 { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } + } + return &httputil.ReverseProxy{Director: director} +} + // proxyPass is the function that assembles a ProxyHandler from the config, a resolver and various options and returns a http.HandlerFunc. func proxyPass(config *RouteHandlerConfig, infoProvider WorkspaceInfoProvider, resolver targetResolver, opts ...proxyPassOpt) http.HandlerFunc { h := proxyPassConfig{ @@ -76,7 +129,8 @@ func proxyPass(config *RouteHandlerConfig, infoProvider WorkspaceInfoProvider, r originalURL := *req.URL // TODO(cw): we should cache the proxy for some time for each target URL - proxy := httputil.NewSingleHostReverseProxy(targetURL) + + proxy := NewSingleHostReverseProxy(targetURL) proxy.Transport = h.Transport proxy.ModifyResponse = func(resp *http.Response) error { url := resp.Request.URL diff --git a/components/ws-proxy/pkg/proxy/routes.go b/components/ws-proxy/pkg/proxy/routes.go index 438001e7f178f1..d7f085758e0bc7 100644 --- a/components/ws-proxy/pkg/proxy/routes.go +++ b/components/ws-proxy/pkg/proxy/routes.go @@ -239,7 +239,7 @@ func resolveSupervisorURL(cfg *Config, info *WorkspaceInfo, req *http.Request) ( var dst url.URL dst.Scheme = cfg.BlobServer.Scheme dst.Host = cfg.BlobServer.Host - dst.Path = "/" + supervisorImage + dst.Path = cfg.BlobServer.PathPrefix + "/" + supervisorImage return &dst, nil } @@ -269,7 +269,7 @@ func (ir *ideRoutes) HandleRoot(route *mux.Route) { return "" } image := info.IDEImage - imagePath := strings.TrimPrefix(req.URL.Path, "/"+image) + imagePath := strings.TrimPrefix(req.URL.Path, t.Config.BlobServer.PathPrefix+"/"+image) if imagePath != "/index.html" && imagePath != "/" { return image } @@ -283,7 +283,7 @@ func (ir *ideRoutes) HandleRoot(route *mux.Route) { return image } supervisorURLString := supervisorURL.String() + "/main.js" - preloadSupervisorReq, err := http.NewRequest("GET", supervisorURLString, nil) + preloadSupervisorReq, err := http.NewRequest("HEAD", supervisorURLString, nil) if err != nil { log.WithField("supervisorURL", supervisorURL).WithError(err).Error("could not preload supervisor") return image @@ -348,7 +348,7 @@ func installBlobserveRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvi var dst url.URL dst.Scheme = cfg.BlobServer.Scheme dst.Host = cfg.BlobServer.Host - dst.Path = image + dst.Path = cfg.BlobServer.PathPrefix + "/" + strings.TrimPrefix(image, "/") return &dst, nil } r.NewRoute().Handler(proxyPass(config, infoProvider, targetResolver, withLongTermCaching())) @@ -424,7 +424,7 @@ func dynamicIDEResolver(config *Config, infoProvider WorkspaceInfoProvider, req var dst url.URL dst.Scheme = config.BlobServer.Scheme dst.Host = config.BlobServer.Host - dst.Path = "/" + info.IDEImage + dst.Path = config.BlobServer.PathPrefix + "/" + info.IDEImage return &dst, nil } @@ -629,7 +629,7 @@ type blobserveTransport struct { } func (t *blobserveTransport) DoRoundTrip(req *http.Request) (resp *http.Response, err error) { - for { + for i := 0; i < 5; i++ { resp, err = t.transport.RoundTrip(req) if err != nil { return nil, err @@ -702,7 +702,7 @@ func (t *blobserveTransport) RoundTrip(req *http.Request) (resp *http.Response, } func (t *blobserveTransport) redirect(image string, req *http.Request) (*http.Response, error) { - path := strings.TrimPrefix(req.URL.Path, "/"+image) + path := strings.TrimPrefix(req.URL.Path, t.Config.BlobServer.PathPrefix+"/"+image) location := t.asBlobserveURL(image, path) header := make(http.Header, 2) @@ -726,10 +726,9 @@ func (t *blobserveTransport) redirect(image string, req *http.Request) (*http.Re } func (t *blobserveTransport) asBlobserveURL(image string, path string) string { - return fmt.Sprintf("%s://%s%s/%s%s%s", + return fmt.Sprintf("%s://ide.%s/blobserve/%s%s%s", t.Config.GitpodInstallation.Scheme, - "blobserve", - t.Config.GitpodInstallation.WorkspaceHostSuffix, + t.Config.GitpodInstallation.HostName, image, imagePathSeparator, path, diff --git a/components/ws-proxy/pkg/proxy/routes_test.go b/components/ws-proxy/pkg/proxy/routes_test.go index ea3f9b5dd9f0e2..1621af562d57c4 100644 --- a/components/ws-proxy/pkg/proxy/routes_test.go +++ b/components/ws-proxy/pkg/proxy/routes_test.go @@ -224,11 +224,11 @@ func TestRoutes(t *testing.T) { Header: http.Header{ "Content-Type": {"text/html; charset=utf-8"}, "Location": { - "https://blobserve.ws.test-domain.com/gitpod-io/supervisor:latest/__files__/favicon.ico", + "https://ide.test-domain.com/blobserve/gitpod-io/supervisor:latest/__files__/favicon.ico", }, "Vary": {"Accept-Encoding"}, }, - Body: "See Other.\n\n", + Body: "See Other.\n\n", }, }, { @@ -241,10 +241,10 @@ func TestRoutes(t *testing.T) { Status: http.StatusSeeOther, Header: http.Header{ "Content-Type": {"text/html; charset=utf-8"}, - "Location": {"https://blobserve.ws.test-domain.com/gitpod-io/ide:latest/__files__/"}, + "Location": {"https://ide.test-domain.com/blobserve/gitpod-io/ide:latest/__files__/"}, "Vary": {"Accept-Encoding"}, }, - Body: "See Other.\n\n", + Body: "See Other.\n\n", }, }, { @@ -257,11 +257,11 @@ func TestRoutes(t *testing.T) { Expectation: Expectation{ Status: http.StatusOK, Header: http.Header{ - "Content-Length": {"218"}, + "Content-Length": {"220"}, "Content-Type": {"text/plain; charset=utf-8"}, "Vary": {"Accept-Encoding"}, }, - Body: "blobserve hit: /gitpod-io/ide:latest/\ninlineVars: {\"ide\":\"https://blobserve.ws.test-domain.com/gitpod-io/ide:latest/__files__\",\"supervisor\":\"https://blobserve.ws.test-domain.com/gitpod-io/supervisor:latest/__files__\"}\n", + Body: "blobserve hit: /gitpod-io/ide:latest/\ninlineVars: {\"ide\":\"https://ide.test-domain.com/blobserve/gitpod-io/ide:latest/__files__\",\"supervisor\":\"https://ide.test-domain.com/blobserve/gitpod-io/supervisor:latest/__files__\"}\n", }, }, { @@ -274,11 +274,11 @@ func TestRoutes(t *testing.T) { Expectation: Expectation{ Status: http.StatusOK, Header: http.Header{ - "Content-Length": {"218"}, + "Content-Length": {"220"}, "Content-Type": {"text/plain; charset=utf-8"}, "Vary": {"Accept-Encoding"}, }, - Body: "blobserve hit: /gitpod-io/ide:latest/\ninlineVars: {\"ide\":\"https://blobserve.ws.test-domain.com/gitpod-io/ide:latest/__files__\",\"supervisor\":\"https://blobserve.ws.test-domain.com/gitpod-io/supervisor:latest/__files__\"}\n", + Body: "blobserve hit: /gitpod-io/ide:latest/\ninlineVars: {\"ide\":\"https://ide.test-domain.com/blobserve/gitpod-io/ide:latest/__files__\",\"supervisor\":\"https://ide.test-domain.com/blobserve/gitpod-io/supervisor:latest/__files__\"}\n", }, }, { @@ -467,10 +467,10 @@ func TestRoutes(t *testing.T) { Status: http.StatusSeeOther, Header: http.Header{ "Content-Type": {"text/html; charset=utf-8"}, - "Location": {"https://blobserve.ws.test-domain.com/gitpod-io/supervisor:latest/__files__/main.js"}, + "Location": {"https://ide.test-domain.com/blobserve/gitpod-io/supervisor:latest/__files__/main.js"}, "Vary": {"Accept-Encoding"}, }, - Body: "See Other.\n\n", + Body: "See Other.\n\n", }, }, { @@ -493,10 +493,10 @@ func TestRoutes(t *testing.T) { Status: http.StatusSeeOther, Header: http.Header{ "Content-Type": {"text/html; charset=utf-8"}, - "Location": {"https://blobserve.ws.test-domain.com/gitpod-io/supervisor:latest/__files__/main.js"}, + "Location": {"https://ide.test-domain.com/blobserve/gitpod-io/supervisor:latest/__files__/main.js"}, "Vary": {"Accept-Encoding"}, }, - Body: "See Other.\n\n", + Body: "See Other.\n\n", }, }, { diff --git a/install/installer/pkg/components/ws-proxy/configmap.go b/install/installer/pkg/components/ws-proxy/configmap.go index 0501990fe454a9..b22421a2491ffe 100644 --- a/install/installer/pkg/components/ws-proxy/configmap.go +++ b/install/installer/pkg/components/ws-proxy/configmap.go @@ -44,8 +44,9 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { MaxIdleConnsPerHost: 100, }, BlobServer: &proxy.BlobServerConfig{ - Scheme: "http", - Host: fmt.Sprintf("blobserve.%s.svc.cluster.local:%d", ctx.Namespace, common.BlobServeServicePort), + Scheme: "https", + Host: fmt.Sprintf("ide.%s", ctx.Config.Domain), + PathPrefix: "/blobserve", }, GitpodInstallation: &proxy.GitpodInstallation{ Scheme: "https",