Skip to content

Commit c8b3074

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

File tree

6 files changed

+190
-19
lines changed

6 files changed

+190
-19
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 ad96a5a674ce1c58635b1d4907d48100121610b9
45+
ENV GP_CODE_COMMIT 26db64e39fb52c4dcc36b663625c47a0db96acb7
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-) in 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-) in 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 ^(.*)(webview|extensions).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/routes.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ 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+
return m.Vars != nil && m.Vars[foreignOriginIdentifier] != ""
108108
}))
109109

110110
routes.HandleRoot(r.NewRoute())
@@ -132,7 +132,7 @@ func (ir *ideRoutes) HandleDirectIDERoute(route *mux.Route) {
132132
r.Use(ir.Config.WorkspaceAuthHandler)
133133
r.Use(ir.workspaceMustExistHandler)
134134

135-
r.NewRoute().HandlerFunc(proxyPass(ir.Config, workspacePodResolver))
135+
r.NewRoute().HandlerFunc(proxyPass(ir.Config, workspacePodResolver, withWorkspaceTransport()))
136136
}
137137

138138
func (ir *ideRoutes) HandleDirectSupervisorRoute(route *mux.Route, authenticated bool) {
@@ -323,6 +323,7 @@ func installWorkspacePortRoutes(r *mux.Router, config *RouteHandlerConfig) error
323323
workspacePodPortResolver,
324324
withHTTPErrorHandler(showPortNotFoundPage),
325325
withXFrameOptionsFilter(),
326+
withWorkspaceTransport(),
326327
)(rw, r)
327328
},
328329
)
@@ -707,3 +708,22 @@ func servePortNotFoundPage(config *Config) (http.Handler, error) {
707708
w.Write(page)
708709
}), nil
709710
}
711+
712+
type workspaceTransport struct {
713+
transport http.RoundTripper
714+
}
715+
716+
func (t *workspaceTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
717+
vars := mux.Vars(req)
718+
if vars[foreignPathIdentifier] != "" {
719+
req = req.Clone(req.Context())
720+
req.URL.Path = vars[foreignPathIdentifier]
721+
}
722+
return t.transport.RoundTrip(req)
723+
}
724+
725+
func withWorkspaceTransport() proxyPassOpt {
726+
return func(h *proxyPassConfig) {
727+
h.Transport = &workspaceTransport{h.Transport}
728+
}
729+
}

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.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: 65 additions & 6 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"
@@ -72,14 +75,39 @@ func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) Work
7275
type hostHeaderProvider func(req *http.Request) string
7376

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

82-
matches := r.FindStringSubmatch(hostname)
88+
matches := foreignContentHostR.FindStringSubmatch(hostname)
89+
if len(matches) == 2 {
90+
foreignOrigin := matches[1]
91+
matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
92+
if len(matches) < 3 {
93+
return false
94+
}
95+
96+
workspaceID := matches[1]
97+
if workspaceID == "" {
98+
return false
99+
}
100+
101+
if m.Vars == nil {
102+
m.Vars = make(map[string]string)
103+
}
104+
m.Vars[workspaceIDIdentifier] = workspaceID
105+
m.Vars[foreignOriginIdentifier] = foreignOrigin
106+
m.Vars[foreignPathIdentifier] = matches[2]
107+
return true
108+
}
109+
110+
matches = r.FindStringSubmatch(hostname)
83111
if len(matches) < 3 {
84112
return false
85113
}
@@ -94,21 +122,52 @@ func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProv
94122
}
95123
m.Vars[workspaceIDIdentifier] = workspaceID
96124
if len(matches) == 3 {
97-
m.Vars[foreignOriginPrefix] = matches[1]
125+
m.Vars[foreignOriginIdentifier] = matches[1]
98126
}
99127
return true
100128
}
101129
}
102130

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

111-
matches := r.FindStringSubmatch(hostname)
142+
matches := foreignContentHostR.FindStringSubmatch(hostname)
143+
if len(matches) == 2 {
144+
foreignOrigin := matches[1]
145+
matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
146+
if len(matches) < 4 {
147+
return false
148+
}
149+
150+
workspaceID := matches[2]
151+
if workspaceID == "" {
152+
return false
153+
}
154+
155+
workspacePort := matches[1]
156+
if workspacePort == "" {
157+
return false
158+
}
159+
160+
if m.Vars == nil {
161+
m.Vars = make(map[string]string)
162+
}
163+
m.Vars[workspaceIDIdentifier] = workspaceID
164+
m.Vars[workspacePortIdentifier] = workspacePort
165+
m.Vars[foreignOriginIdentifier] = foreignOrigin
166+
m.Vars[foreignPathIdentifier] = matches[3]
167+
return true
168+
}
169+
170+
matches = r.FindStringSubmatch(hostname)
112171
if len(matches) < 4 {
113172
return false
114173
}
@@ -129,7 +188,7 @@ func matchWorkspacePortHostHeader(wsHostSuffix string, headerProvider hostHeader
129188
m.Vars[workspaceIDIdentifier] = workspaceID
130189
m.Vars[workspacePortIdentifier] = workspacePort
131190
if len(matches) == 4 {
132-
m.Vars[foreignOriginPrefix] = matches[1]
191+
m.Vars[foreignOriginIdentifier] = matches[1]
133192
}
134193
return true
135194
}

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

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package proxy
77
import (
88
"net/http"
99
"net/http/httptest"
10+
"net/url"
1011
"testing"
1112

1213
"github.com/google/go-cmp/cmp"
@@ -144,6 +145,7 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
144145
tests := []struct {
145146
Name string
146147
HostHeader string
148+
Path string
147149
Expected matchResult
148150
}{
149151
{
@@ -160,8 +162,8 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
160162
Expected: matchResult{
161163
MatchesWorkspace: true,
162164
WorkspaceVars: map[string]string{
163-
foreignOriginPrefix: "",
164-
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
165+
foreignOriginIdentifier: "",
166+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
165167
},
166168
},
167169
},
@@ -171,8 +173,34 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
171173
Expected: matchResult{
172174
MatchesWorkspace: true,
173175
WorkspaceVars: map[string]string{
174-
foreignOriginPrefix: "webview-",
175-
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
176+
foreignOriginIdentifier: "webview-",
177+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
178+
},
179+
},
180+
},
181+
{
182+
Name: "unique webview workspace match",
183+
HostHeader: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview" + wsHostSuffix,
184+
Path: "/amaranth-smelt-9ba20cc1/index.html",
185+
Expected: matchResult{
186+
MatchesWorkspace: true,
187+
WorkspaceVars: map[string]string{
188+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
189+
foreignOriginIdentifier: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview",
190+
foreignPathIdentifier: "/index.html",
191+
},
192+
},
193+
},
194+
{
195+
Name: "extension host workspace match",
196+
HostHeader: "extensions" + wsHostSuffix,
197+
Path: "/amaranth-smelt-9ba20cc1/index.html",
198+
Expected: matchResult{
199+
MatchesWorkspace: true,
200+
WorkspaceVars: map[string]string{
201+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
202+
foreignOriginIdentifier: "extensions",
203+
foreignPathIdentifier: "/index.html",
176204
},
177205
},
178206
},
@@ -182,8 +210,8 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
182210
Expected: matchResult{
183211
MatchesWorkspace: true,
184212
WorkspaceVars: map[string]string{
185-
foreignOriginPrefix: "browser-",
186-
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
213+
foreignOriginIdentifier: "browser-",
214+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
187215
},
188216
},
189217
},
@@ -193,7 +221,7 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
193221
Expected: matchResult{
194222
MatchesPort: true,
195223
PortVars: map[string]string{
196-
foreignOriginPrefix: "",
224+
foreignOriginIdentifier: "",
197225
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
198226
workspacePortIdentifier: "8080",
199227
},
@@ -205,19 +233,47 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
205233
Expected: matchResult{
206234
MatchesPort: true,
207235
PortVars: map[string]string{
208-
foreignOriginPrefix: "webview-",
236+
foreignOriginIdentifier: "webview-",
209237
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
210238
workspacePortIdentifier: "8080",
211239
},
212240
},
213241
},
242+
{
243+
Name: "unique webview port match",
244+
HostHeader: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview" + wsHostSuffix,
245+
Path: "/8080-amaranth-smelt-9ba20cc1/index.html",
246+
Expected: matchResult{
247+
MatchesPort: true,
248+
PortVars: map[string]string{
249+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
250+
workspacePortIdentifier: "8080",
251+
foreignOriginIdentifier: "ad859a83-b5a8-43ef-8e82-cfbf36cafacb-webview",
252+
foreignPathIdentifier: "/index.html",
253+
},
254+
},
255+
},
256+
{
257+
Name: "extension host port match",
258+
HostHeader: "extensions" + wsHostSuffix,
259+
Path: "/8080-amaranth-smelt-9ba20cc1/index.html",
260+
Expected: matchResult{
261+
MatchesPort: true,
262+
PortVars: map[string]string{
263+
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
264+
workspacePortIdentifier: "8080",
265+
foreignOriginIdentifier: "extensions",
266+
foreignPathIdentifier: "/index.html",
267+
},
268+
},
269+
},
214270
{
215271
Name: "mini browser port match",
216272
HostHeader: "browser-8080-amaranth-smelt-9ba20cc1" + wsHostSuffix,
217273
Expected: matchResult{
218274
MatchesPort: true,
219275
PortVars: map[string]string{
220-
foreignOriginPrefix: "browser-",
276+
foreignOriginIdentifier: "browser-",
221277
workspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
222278
workspacePortIdentifier: "8080",
223279
},
@@ -227,7 +283,10 @@ func TestMatchWorkspaceHostHeader(t *testing.T) {
227283
for _, test := range tests {
228284
t.Run(test.Name, func(t *testing.T) {
229285
req := &http.Request{
230-
Host: test.HostHeader,
286+
Host: test.HostHeader,
287+
URL: &url.URL{
288+
Path: test.Path,
289+
},
231290
Method: http.MethodGet,
232291
Header: http.Header{
233292
forwardedHostnameHeader: []string{test.HostHeader},

0 commit comments

Comments
 (0)