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 15, 2021
1 parent c060c81 commit f9006f3
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 56 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 a17670ba5af14e0faf3a6927983468d28fda235b
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
20 changes: 20 additions & 0 deletions components/ws-proxy/pkg/proxy/pass.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"syscall"
"time"

"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"

Expand Down Expand Up @@ -187,3 +188,22 @@ func withXFrameOptionsFilter() proxyPassOpt {
})
}
}

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}
}
}
6 changes: 4 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,8 @@ 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] != ""
// this handles all foreign (none-IDE) content
return m.Vars != nil && m.Vars[foreignOriginIdentifier] != ""
}))

routes.HandleRoot(r.NewRoute())
Expand Down Expand Up @@ -132,7 +133,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 +324,7 @@ func installWorkspacePortRoutes(r *mux.Router, config *RouteHandlerConfig) error
workspacePodPortResolver,
withHTTPErrorHandler(showPortNotFoundPage),
withXFrameOptionsFilter(),
withWorkspaceTransport(),
)(rw, r)
},
)
Expand Down
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
135 changes: 93 additions & 42 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 @@ -56,8 +59,8 @@ func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) Work
return req.Header.Get(header)
}
blobserveRouter = r.MatcherFunc(matchBlobserveHostHeader(wsHostSuffix, getHostHeader)).Subrouter()
portRouter = r.MatcherFunc(matchWorkspacePortHostHeader(wsHostSuffix, getHostHeader)).Subrouter()
ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader)).Subrouter()
portRouter = r.MatcherFunc(matchWorkspaceHostHeader(wsHostSuffix, getHostHeader, true)).Subrouter()
ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader, false)).Subrouter()
)

r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
Expand All @@ -71,65 +74,113 @@ func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) Work

type hostHeaderProvider func(req *http.Request) string

func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
r := regexp.MustCompile("^(webview-|browser-|extensions-)?" + workspaceIDRegex + wsHostSuffix)
return func(req *http.Request, m *mux.RouteMatch) bool {
hostname := headerProvider(req)
if hostname == "" {
return false
}

matches := r.FindStringSubmatch(hostname)
if len(matches) < 3 {
return false
}

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

if m.Vars == nil {
m.Vars = make(map[string]string)
}
m.Vars[workspaceIDIdentifier] = workspaceID
if len(matches) == 3 {
m.Vars[foreignOriginPrefix] = matches[1]
}
return true
func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider, matchPort bool) mux.MatcherFunc {
regexPrefix := workspaceIDRegex
if matchPort {
regexPrefix = workspacePortRegex + workspaceIDRegex
}
}

func matchWorkspacePortHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
r := regexp.MustCompile("^(webview-|browser-|extensions-)?" + workspacePortRegex + workspaceIDRegex + wsHostSuffix)
// 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-)?" + regexPrefix + wsHostSuffix)
foreignContentHostR := regexp.MustCompile("^(.+)(?:foreign)" + wsHostSuffix)
foreignContentPathR := regexp.MustCompile("^/" + regexPrefix + "(/.*)")
return func(req *http.Request, m *mux.RouteMatch) bool {
hostname := headerProvider(req)
if hostname == "" {
return false
}

matches := r.FindStringSubmatch(hostname)
if len(matches) < 4 {
return false
var workspaceID, workspacePort, foreignOrigin, foreignPath string
matches := foreignContentHostR.FindStringSubmatch(hostname)
if len(matches) == 2 {
foreignOrigin = matches[1]
matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
if matchPort {
if len(matches) < 4 {
return false
}
// https://extensions-foreign.ws-eu10.gitpod.io/3000-coral-dragon-ilr0r6eq/index.html
// workspaceID: coral-dragon-ilr0r6eq
// workspacePort: 3000
// foreignOrigin: extensions-
// foreignPath: /index.html
workspaceID = matches[2]
workspacePort = matches[1]
foreignPath = matches[3]
} else {
if len(matches) < 3 {
return false
}
// https://extensions-foreign.ws-eu10.gitpod.io/coral-dragon-ilr0r6eq/index.html
// workspaceID: coral-dragon-ilr0r6eq
// workspacePort:
// foreignOrigin: extensions-
// foreignPath: /index.html
workspaceID = matches[1]
foreignPath = matches[2]
}
} else {
matches = r.FindStringSubmatch(hostname)
if matchPort {
if len(matches) < 4 {
return false
}
// https://3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
// workspaceID: coral-dragon-ilr0r6eq
// workspacePort: 3000
// foreignOrigin:
// foreignPath:
workspaceID = matches[3]
workspacePort = matches[2]
if len(matches) == 4 {
// https://extensions-3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
// workspaceID: coral-dragon-ilr0r6eq
// workspacePort: 3000
// foreignOrigin: extensions-
// foreignPath:
foreignOrigin = matches[1]
}
} else {
if len(matches) < 3 {
return false
}
// https://coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
// workspaceID: coral-dragon-ilr0r6eq
// workspacePort:
// foreignOrigin:
// foreignPath:
workspaceID = matches[2]
if len(matches) == 3 {
// https://extensions-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
// workspaceID: coral-dragon-ilr0r6eq
// workspacePort:
// foreignOrigin: extensions-
// foreignPath:
foreignOrigin = matches[1]
}
}
}

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

workspacePort := matches[2]
if workspacePort == "" {
if matchPort && workspacePort == "" {
return false
}

if m.Vars == nil {
m.Vars = make(map[string]string)
}
m.Vars[workspaceIDIdentifier] = workspaceID
m.Vars[workspacePortIdentifier] = workspacePort
if len(matches) == 4 {
m.Vars[foreignOriginPrefix] = matches[1]
if workspacePort != "" {
m.Vars[workspacePortIdentifier] = workspacePort
}
if foreignOrigin != "" {
m.Vars[foreignOriginIdentifier] = foreignOrigin
}
if foreignPath != "" {
m.Vars[foreignPathIdentifier] = foreignPath
}
return true
}
Expand Down
Loading

0 comments on commit f9006f3

Please sign in to comment.