Skip to content

Commit f9006f3

Browse files
committed
[code] fix #4529: serve each webview from own origin
decoupled from workpace origin (also extension host origin)
1 parent c060c81 commit f9006f3

File tree

7 files changed

+219
-56
lines changed

7 files changed

+219
-56
lines changed

components/ide/code/leeway.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ RUN curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh |
4242
&& npm install -g yarn node-gyp
4343
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
4444

45-
ENV GP_CODE_COMMIT a17670ba5af14e0faf3a6927983468d28fda235b
45+
ENV GP_CODE_COMMIT cd2128eb3d1dc89568ccdcffbda30eec2358ea75
4646
RUN mkdir gp-code \
4747
&& cd gp-code \
4848
&& git init \

components/proxy/conf/Caddyfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ https://*.*.{$GITPOD_DOMAIN} {
243243
}
244244
}
245245

246+
# remove (webview-|browser-|extensions-) after Theia removed and new VS Code is used by all workspaces
246247
@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}
247248
handle @workspace_port {
248249
reverse_proxy https://ws-proxy.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9090 {
@@ -255,6 +256,7 @@ https://*.*.{$GITPOD_DOMAIN} {
255256
}
256257
}
257258

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

271+
# foreign content origin should be decoupled from the workspace (port) origin but the workspace (port) prefix should be the path root for routing
272+
@foreign_content header_regexp host Host ^(.*)(foreign).ws(-[a-z0-9]+)?.{$GITPOD_DOMAIN}
273+
handle @foreign_content {
274+
reverse_proxy https://ws-proxy.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:9090 {
275+
import workspace_transport
276+
import upstream_headers
277+
278+
header_up X-WSProxy-Host {http.request.host}
279+
}
280+
}
281+
269282
respond "Not found" 404
270283
}
271284

components/ws-proxy/pkg/proxy/pass.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"syscall"
1414
"time"
1515

16+
"github.com/gorilla/mux"
1617
"github.com/sirupsen/logrus"
1718
"golang.org/x/xerrors"
1819

@@ -187,3 +188,22 @@ func withXFrameOptionsFilter() proxyPassOpt {
187188
})
188189
}
189190
}
191+
192+
type workspaceTransport struct {
193+
transport http.RoundTripper
194+
}
195+
196+
func (t *workspaceTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
197+
vars := mux.Vars(req)
198+
if vars[foreignPathIdentifier] != "" {
199+
req = req.Clone(req.Context())
200+
req.URL.Path = vars[foreignPathIdentifier]
201+
}
202+
return t.transport.RoundTrip(req)
203+
}
204+
205+
func withWorkspaceTransport() proxyPassOpt {
206+
return func(h *proxyPassConfig) {
207+
h.Transport = &workspaceTransport{h.Transport}
208+
}
209+
}

components/ws-proxy/pkg/proxy/routes.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip Worksp
104104
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor"), true)
105105

106106
routes.HandleDirectIDERoute(r.MatcherFunc(func(req *http.Request, m *mux.RouteMatch) bool {
107-
return m.Vars != nil && m.Vars[foreignOriginPrefix] != ""
107+
// this handles all foreign (none-IDE) content
108+
return m.Vars != nil && m.Vars[foreignOriginIdentifier] != ""
108109
}))
109110

110111
routes.HandleRoot(r.NewRoute())
@@ -132,7 +133,7 @@ func (ir *ideRoutes) HandleDirectIDERoute(route *mux.Route) {
132133
r.Use(ir.Config.WorkspaceAuthHandler)
133134
r.Use(ir.workspaceMustExistHandler)
134135

135-
r.NewRoute().HandlerFunc(proxyPass(ir.Config, workspacePodResolver))
136+
r.NewRoute().HandlerFunc(proxyPass(ir.Config, workspacePodResolver, withWorkspaceTransport()))
136137
}
137138

138139
func (ir *ideRoutes) HandleDirectSupervisorRoute(route *mux.Route, authenticated bool) {
@@ -323,6 +324,7 @@ func installWorkspacePortRoutes(r *mux.Router, config *RouteHandlerConfig) error
323324
workspacePodPortResolver,
324325
withHTTPErrorHandler(showPortNotFoundPage),
325326
withXFrameOptionsFilter(),
327+
withWorkspaceTransport(),
326328
)(rw, r)
327329
},
328330
)

components/ws-proxy/pkg/proxy/routes_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,26 @@ func TestRoutes(t *testing.T) {
580580
Body: "blobserve hit: /blobserve/gitpod-io/supervisor:latest/main.js\nreadOnly: true\n",
581581
},
582582
},
583+
{
584+
Desc: "extensions route GET",
585+
Request: modifyRequest(httptest.NewRequest("GET", "https://extensions-foreign.test-domain.com/"+workspaces[0].WorkspaceID+"/extensions.js", nil),
586+
addHostHeader,
587+
addHeader("Origin", config.GitpodInstallation.HostName),
588+
addOwnerToken(workspaces[0].InstanceID, workspaces[0].Auth.OwnerToken),
589+
),
590+
Expectation: Expectation{
591+
Status: http.StatusOK,
592+
Header: http.Header{
593+
"Access-Control-Allow-Credentials": {"true"},
594+
"Access-Control-Allow-Origin": {"test-domain.com"},
595+
"Access-Control-Expose-Headers": {"Authorization"},
596+
"Content-Length": {"30"},
597+
"Content-Type": {"text/plain; charset=utf-8"},
598+
"Vary": {"Accept-Encoding"},
599+
},
600+
Body: "workspace hit: /extensions.js\n",
601+
},
602+
},
583603
}
584604

585605
log.Init("ws-proxy-test", "", false, true)

components/ws-proxy/pkg/proxy/workspacerouter.go

Lines changed: 93 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ const (
2121
// Used as key for storing the workspace ID in the requests mux.Vars() map
2222
workspaceIDIdentifier = "workspaceID"
2323

24-
// Used as key for storing the origin prefix to fetch foreign content
25-
foreignOriginPrefix = "foreignOriginPrefix"
24+
// Used as key for storing the origin to fetch foreign content
25+
foreignOriginIdentifier = "foreignOrigin"
26+
27+
// Used as key for storing the path to fetch foreign content
28+
foreignPathIdentifier = "foreignPath"
2629

2730
// The header that is used to communicate the "Host" from proxy -> ws-proxy in scenarios where ws-proxy is _not_ directly exposed
2831
forwardedHostnameHeader = "x-wsproxy-host"
@@ -56,8 +59,8 @@ func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) Work
5659
return req.Header.Get(header)
5760
}
5861
blobserveRouter = r.MatcherFunc(matchBlobserveHostHeader(wsHostSuffix, getHostHeader)).Subrouter()
59-
portRouter = r.MatcherFunc(matchWorkspacePortHostHeader(wsHostSuffix, getHostHeader)).Subrouter()
60-
ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader)).Subrouter()
62+
portRouter = r.MatcherFunc(matchWorkspaceHostHeader(wsHostSuffix, getHostHeader, true)).Subrouter()
63+
ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader, false)).Subrouter()
6164
)
6265

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

7275
type hostHeaderProvider func(req *http.Request) string
7376

74-
func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
75-
r := regexp.MustCompile("^(webview-|browser-|extensions-)?" + workspaceIDRegex + wsHostSuffix)
76-
return func(req *http.Request, m *mux.RouteMatch) bool {
77-
hostname := headerProvider(req)
78-
if hostname == "" {
79-
return false
80-
}
81-
82-
matches := r.FindStringSubmatch(hostname)
83-
if len(matches) < 3 {
84-
return false
85-
}
86-
87-
workspaceID := matches[2]
88-
if workspaceID == "" {
89-
return false
90-
}
91-
92-
if m.Vars == nil {
93-
m.Vars = make(map[string]string)
94-
}
95-
m.Vars[workspaceIDIdentifier] = workspaceID
96-
if len(matches) == 3 {
97-
m.Vars[foreignOriginPrefix] = matches[1]
98-
}
99-
return true
77+
func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider, matchPort bool) mux.MatcherFunc {
78+
regexPrefix := workspaceIDRegex
79+
if matchPort {
80+
regexPrefix = workspacePortRegex + workspaceIDRegex
10081
}
101-
}
10282

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

111-
matches := r.FindStringSubmatch(hostname)
112-
if len(matches) < 4 {
113-
return false
93+
var workspaceID, workspacePort, foreignOrigin, foreignPath string
94+
matches := foreignContentHostR.FindStringSubmatch(hostname)
95+
if len(matches) == 2 {
96+
foreignOrigin = matches[1]
97+
matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
98+
if matchPort {
99+
if len(matches) < 4 {
100+
return false
101+
}
102+
// https://extensions-foreign.ws-eu10.gitpod.io/3000-coral-dragon-ilr0r6eq/index.html
103+
// workspaceID: coral-dragon-ilr0r6eq
104+
// workspacePort: 3000
105+
// foreignOrigin: extensions-
106+
// foreignPath: /index.html
107+
workspaceID = matches[2]
108+
workspacePort = matches[1]
109+
foreignPath = matches[3]
110+
} else {
111+
if len(matches) < 3 {
112+
return false
113+
}
114+
// https://extensions-foreign.ws-eu10.gitpod.io/coral-dragon-ilr0r6eq/index.html
115+
// workspaceID: coral-dragon-ilr0r6eq
116+
// workspacePort:
117+
// foreignOrigin: extensions-
118+
// foreignPath: /index.html
119+
workspaceID = matches[1]
120+
foreignPath = matches[2]
121+
}
122+
} else {
123+
matches = r.FindStringSubmatch(hostname)
124+
if matchPort {
125+
if len(matches) < 4 {
126+
return false
127+
}
128+
// https://3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
129+
// workspaceID: coral-dragon-ilr0r6eq
130+
// workspacePort: 3000
131+
// foreignOrigin:
132+
// foreignPath:
133+
workspaceID = matches[3]
134+
workspacePort = matches[2]
135+
if len(matches) == 4 {
136+
// https://extensions-3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
137+
// workspaceID: coral-dragon-ilr0r6eq
138+
// workspacePort: 3000
139+
// foreignOrigin: extensions-
140+
// foreignPath:
141+
foreignOrigin = matches[1]
142+
}
143+
} else {
144+
if len(matches) < 3 {
145+
return false
146+
}
147+
// https://coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
148+
// workspaceID: coral-dragon-ilr0r6eq
149+
// workspacePort:
150+
// foreignOrigin:
151+
// foreignPath:
152+
workspaceID = matches[2]
153+
if len(matches) == 3 {
154+
// https://extensions-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
155+
// workspaceID: coral-dragon-ilr0r6eq
156+
// workspacePort:
157+
// foreignOrigin: extensions-
158+
// foreignPath:
159+
foreignOrigin = matches[1]
160+
}
161+
}
114162
}
115163

116-
workspaceID := matches[3]
117164
if workspaceID == "" {
118165
return false
119166
}
120167

121-
workspacePort := matches[2]
122-
if workspacePort == "" {
168+
if matchPort && workspacePort == "" {
123169
return false
124170
}
125171

126172
if m.Vars == nil {
127173
m.Vars = make(map[string]string)
128174
}
129175
m.Vars[workspaceIDIdentifier] = workspaceID
130-
m.Vars[workspacePortIdentifier] = workspacePort
131-
if len(matches) == 4 {
132-
m.Vars[foreignOriginPrefix] = matches[1]
176+
if workspacePort != "" {
177+
m.Vars[workspacePortIdentifier] = workspacePort
178+
}
179+
if foreignOrigin != "" {
180+
m.Vars[foreignOriginIdentifier] = foreignOrigin
181+
}
182+
if foreignPath != "" {
183+
m.Vars[foreignPathIdentifier] = foreignPath
133184
}
134185
return true
135186
}

0 commit comments

Comments
 (0)