Skip to content

Commit

Permalink
[code] fix #4529: serve each webview from own origin
Browse files Browse the repository at this point in the history
decoupled from workpace origin (also extension host origin)
  • Loading branch information
akosyakov committed Jul 13, 2021
1 parent 4f59081 commit ae57f7d
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 19 deletions.
2 changes: 1 addition & 1 deletion components/ide/code/leeway.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ RUN curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh |
&& npm install -g yarn node-gyp
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH

ENV GP_CODE_COMMIT ad96a5a674ce1c58635b1d4907d48100121610b9
ENV GP_CODE_COMMIT cd2128eb3d1dc89568ccdcffbda30eec2358ea75
RUN mkdir gp-code \
&& cd gp-code \
&& git init \
Expand Down
13 changes: 13 additions & 0 deletions components/proxy/conf/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ https://*.*.{$GITPOD_DOMAIN} {
}
}

# remove (webview-|browser-|extensions-) after Theia removed and new VS Code is used by all workspaces
@workspace_port header_regexp host Host ^(webview-|browser-|extensions-)?(?P<workspacePort>[0-9]{2,5})-(?P<workspaceID>[a-z0-9][0-9a-z\-]+).ws(?P<location>-[a-z0-9]+)?.{$GITPOD_DOMAIN}
handle @workspace_port {
reverse_proxy https://ws-proxy.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9090 {
Expand All @@ -255,6 +256,7 @@ https://*.*.{$GITPOD_DOMAIN} {
}
}

# remove (webview-|browser-|extensions-) after Theia removed and new VS Code is used by all workspaces
@workspace header_regexp host Host ^(webview-|browser-|extensions-)?(?P<workspaceID>[a-z0-9][0-9a-z\-]+).ws(?P<location>-[a-z0-9]+)?.{$GITPOD_DOMAIN}
handle @workspace {
reverse_proxy https://ws-proxy.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9090 {
Expand All @@ -266,6 +268,17 @@ https://*.*.{$GITPOD_DOMAIN} {
}
}

# foreign content origin should be decoupled from the workspace (port) origin but the workspace (port) prefix should be the path root for routing
@foreign_content header_regexp host Host ^(.*)(foreign).ws(-[a-z0-9]+)?.{$GITPOD_DOMAIN}
handle @foreign_content {
reverse_proxy https://ws-proxy.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9090 {
import workspace_transport
import upstream_headers

header_up X-WSProxy-Host {http.request.host}
}
}

respond "Not found" 404
}

Expand Down
24 changes: 22 additions & 2 deletions components/ws-proxy/pkg/proxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip Worksp
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor"), true)

routes.HandleDirectIDERoute(r.MatcherFunc(func(req *http.Request, m *mux.RouteMatch) bool {
return m.Vars != nil && m.Vars[foreignOriginPrefix] != ""
return m.Vars != nil && m.Vars[foreignOriginIdentifier] != ""
}))

routes.HandleRoot(r.NewRoute())
Expand Down Expand Up @@ -132,7 +132,7 @@ func (ir *ideRoutes) HandleDirectIDERoute(route *mux.Route) {
r.Use(ir.Config.WorkspaceAuthHandler)
r.Use(ir.workspaceMustExistHandler)

r.NewRoute().HandlerFunc(proxyPass(ir.Config, workspacePodResolver))
r.NewRoute().HandlerFunc(proxyPass(ir.Config, workspacePodResolver, withWorkspaceTransport()))
}

func (ir *ideRoutes) HandleDirectSupervisorRoute(route *mux.Route, authenticated bool) {
Expand Down Expand Up @@ -323,6 +323,7 @@ func installWorkspacePortRoutes(r *mux.Router, config *RouteHandlerConfig) error
workspacePodPortResolver,
withHTTPErrorHandler(showPortNotFoundPage),
withXFrameOptionsFilter(),
withWorkspaceTransport(),
)(rw, r)
},
)
Expand Down Expand Up @@ -707,3 +708,22 @@ func servePortNotFoundPage(config *Config) (http.Handler, error) {
w.Write(page)
}), nil
}

type workspaceTransport struct {
transport http.RoundTripper
}

func (t *workspaceTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
vars := mux.Vars(req)
if vars[foreignPathIdentifier] != "" {
req = req.Clone(req.Context())
req.URL.Path = vars[foreignPathIdentifier]
}
return t.transport.RoundTrip(req)
}

func withWorkspaceTransport() proxyPassOpt {
return func(h *proxyPassConfig) {
h.Transport = &workspaceTransport{h.Transport}
}
}
20 changes: 20 additions & 0 deletions components/ws-proxy/pkg/proxy/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,26 @@ func TestRoutes(t *testing.T) {
Body: "blobserve hit: /blobserve/gitpod-io/supervisor:latest/main.js\nreadOnly: true\n",
},
},
{
Desc: "extensions route GET",
Request: modifyRequest(httptest.NewRequest("GET", "https://extensions-foreign.test-domain.com/"+workspaces[0].WorkspaceID+"/extensions.js", nil),
addHostHeader,
addHeader("Origin", config.GitpodInstallation.HostName),
addOwnerToken(workspaces[0].InstanceID, workspaces[0].Auth.OwnerToken),
),
Expectation: Expectation{
Status: http.StatusOK,
Header: http.Header{
"Access-Control-Allow-Credentials": {"true"},
"Access-Control-Allow-Origin": {"test-domain.com"},
"Access-Control-Expose-Headers": {"Authorization"},
"Content-Length": {"30"},
"Content-Type": {"text/plain; charset=utf-8"},
"Vary": {"Accept-Encoding"},
},
Body: "workspace hit: /extensions.js\n",
},
},
}

log.Init("ws-proxy-test", "", false, true)
Expand Down
71 changes: 65 additions & 6 deletions components/ws-proxy/pkg/proxy/workspacerouter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ const (
// Used as key for storing the workspace ID in the requests mux.Vars() map
workspaceIDIdentifier = "workspaceID"

// Used as key for storing the origin prefix to fetch foreign content
foreignOriginPrefix = "foreignOriginPrefix"
// Used as key for storing the origin to fetch foreign content
foreignOriginIdentifier = "foreignOrigin"

// Used as key for storing the path to fetch foreign content
foreignPathIdentifier = "foreignPath"

// The header that is used to communicate the "Host" from proxy -> ws-proxy in scenarios where ws-proxy is _not_ directly exposed
forwardedHostnameHeader = "x-wsproxy-host"
Expand Down Expand Up @@ -72,14 +75,39 @@ func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) Work
type hostHeaderProvider func(req *http.Request) string

func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
// remove (webview-|browser-|extensions-) prefix as soon as Theia removed and new VS Code is used in all workspaces
r := regexp.MustCompile("^(webview-|browser-|extensions-)?" + workspaceIDRegex + wsHostSuffix)
foreignContentHostR := regexp.MustCompile("^(.*)(?:foreign)" + wsHostSuffix)
foreignContentPathR := regexp.MustCompile("^/" + workspaceIDRegex + "(/.*)")
return func(req *http.Request, m *mux.RouteMatch) bool {
hostname := headerProvider(req)
if hostname == "" {
return false
}

matches := r.FindStringSubmatch(hostname)
matches := foreignContentHostR.FindStringSubmatch(hostname)
if len(matches) == 2 {
foreignOrigin := matches[1]
matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
if len(matches) < 3 {
return false
}

workspaceID := matches[1]
if workspaceID == "" {
return false
}

if m.Vars == nil {
m.Vars = make(map[string]string)
}
m.Vars[workspaceIDIdentifier] = workspaceID
m.Vars[foreignOriginIdentifier] = foreignOrigin
m.Vars[foreignPathIdentifier] = matches[2]
return true
}

matches = r.FindStringSubmatch(hostname)
if len(matches) < 3 {
return false
}
Expand All @@ -94,21 +122,52 @@ func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProv
}
m.Vars[workspaceIDIdentifier] = workspaceID
if len(matches) == 3 {
m.Vars[foreignOriginPrefix] = matches[1]
m.Vars[foreignOriginIdentifier] = matches[1]
}
return true
}
}

func matchWorkspacePortHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
// remove (webview-|browser-|extensions-) prefix as soon as Theia removed and new VS Code is used in all workspaces
r := regexp.MustCompile("^(webview-|browser-|extensions-)?" + workspacePortRegex + workspaceIDRegex + wsHostSuffix)
foreignContentHostR := regexp.MustCompile("^(.*)(?:foreign)" + wsHostSuffix)
foreignContentPathR := regexp.MustCompile("^/" + workspacePortRegex + workspaceIDRegex + "(/.*)")
return func(req *http.Request, m *mux.RouteMatch) bool {
hostname := headerProvider(req)
if hostname == "" {
return false
}

matches := r.FindStringSubmatch(hostname)
matches := foreignContentHostR.FindStringSubmatch(hostname)
if len(matches) == 2 {
foreignOrigin := matches[1]
matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
if len(matches) < 4 {
return false
}

workspaceID := matches[2]
if workspaceID == "" {
return false
}

workspacePort := matches[1]
if workspacePort == "" {
return false
}

if m.Vars == nil {
m.Vars = make(map[string]string)
}
m.Vars[workspaceIDIdentifier] = workspaceID
m.Vars[workspacePortIdentifier] = workspacePort
m.Vars[foreignOriginIdentifier] = foreignOrigin
m.Vars[foreignPathIdentifier] = matches[3]
return true
}

matches = r.FindStringSubmatch(hostname)
if len(matches) < 4 {
return false
}
Expand All @@ -129,7 +188,7 @@ func matchWorkspacePortHostHeader(wsHostSuffix string, headerProvider hostHeader
m.Vars[workspaceIDIdentifier] = workspaceID
m.Vars[workspacePortIdentifier] = workspacePort
if len(matches) == 4 {
m.Vars[foreignOriginPrefix] = matches[1]
m.Vars[foreignOriginIdentifier] = matches[1]
}
return true
}
Expand Down
79 changes: 69 additions & 10 deletions components/ws-proxy/pkg/proxy/workspacerouter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package proxy
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -144,6 +145,7 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
tests := []struct {
Name string
HostHeader string
Path string
Expected matchResult
}{
{
Expand All @@ -160,8 +162,8 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
Expected: matchResult{
MatchesWorkspace: true,
WorkspaceVars: map[string]string{
foreignOriginPrefix: "",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
foreignOriginIdentifier: "",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
},
},
},
Expand All @@ -171,8 +173,34 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
Expected: matchResult{
MatchesWorkspace: true,
WorkspaceVars: map[string]string{
foreignOriginPrefix: "webview-",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
foreignOriginIdentifier: "webview-",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
},
},
},
{
Name: "unique webview workspace match",
HostHeader: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview-foreign" + wsHostSuffix,
Path: "/amaranth-smelt-9ba20cc1/index.html",
Expected: matchResult{
MatchesWorkspace: true,
WorkspaceVars: map[string]string{
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
foreignOriginIdentifier: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview-",
foreignPathIdentifier: "/index.html",
},
},
},
{
Name: "extension host workspace match",
HostHeader: "extensions-foreign" + wsHostSuffix,
Path: "/amaranth-smelt-9ba20cc1/index.html",
Expected: matchResult{
MatchesWorkspace: true,
WorkspaceVars: map[string]string{
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
foreignOriginIdentifier: "extensions-",
foreignPathIdentifier: "/index.html",
},
},
},
Expand All @@ -182,8 +210,8 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
Expected: matchResult{
MatchesWorkspace: true,
WorkspaceVars: map[string]string{
foreignOriginPrefix: "browser-",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
foreignOriginIdentifier: "browser-",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
},
},
},
Expand All @@ -193,7 +221,7 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
Expected: matchResult{
MatchesPort: true,
PortVars: map[string]string{
foreignOriginPrefix: "",
foreignOriginIdentifier: "",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
workspacePortIdentifier: "8080",
},
Expand All @@ -205,19 +233,47 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
Expected: matchResult{
MatchesPort: true,
PortVars: map[string]string{
foreignOriginPrefix: "webview-",
foreignOriginIdentifier: "webview-",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
workspacePortIdentifier: "8080",
},
},
},
{
Name: "unique webview port match",
HostHeader: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview-foreign" + wsHostSuffix,
Path: "/8080-amaranth-smelt-9ba20cc1/index.html",
Expected: matchResult{
MatchesPort: true,
PortVars: map[string]string{
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
workspacePortIdentifier: "8080",
foreignOriginIdentifier: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview-",
foreignPathIdentifier: "/index.html",
},
},
},
{
Name: "extension host port match",
HostHeader: "extensions-foreign" + wsHostSuffix,
Path: "/8080-amaranth-smelt-9ba20cc1/index.html",
Expected: matchResult{
MatchesPort: true,
PortVars: map[string]string{
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
workspacePortIdentifier: "8080",
foreignOriginIdentifier: "extensions-",
foreignPathIdentifier: "/index.html",
},
},
},
{
Name: "mini browser port match",
HostHeader: "browser-8080-amaranth-smelt-9ba20cc1" + wsHostSuffix,
Expected: matchResult{
MatchesPort: true,
PortVars: map[string]string{
foreignOriginPrefix: "browser-",
foreignOriginIdentifier: "browser-",
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
workspacePortIdentifier: "8080",
},
Expand All @@ -227,7 +283,10 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
req := &http.Request{
Host: test.HostHeader,
Host: test.HostHeader,
URL: &url.URL{
Path: test.Path,
},
Method: http.MethodGet,
Header: http.Header{
forwardedHostnameHeader: []string{test.HostHeader},
Expand Down

0 comments on commit ae57f7d

Please sign in to comment.