Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-proxy/pkg/proxy/routes.go
2500 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package proxy
6
7
import (
8
"bytes"
9
"context"
10
"crypto/ecdsa"
11
"crypto/elliptic"
12
crand "crypto/rand"
13
"encoding/base64"
14
"encoding/json"
15
"encoding/pem"
16
"fmt"
17
"io"
18
"math/rand"
19
"net/http"
20
"net/url"
21
"os"
22
"path/filepath"
23
"regexp"
24
"strconv"
25
"strings"
26
"time"
27
28
"github.com/gorilla/websocket"
29
30
"github.com/gitpod-io/golang-crypto/ssh"
31
"github.com/gorilla/handlers"
32
"github.com/gorilla/mux"
33
"github.com/sirupsen/logrus"
34
"golang.org/x/xerrors"
35
36
"github.com/gitpod-io/gitpod/common-go/log"
37
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
38
"github.com/gitpod-io/gitpod/ws-manager/api"
39
"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"
40
"github.com/gitpod-io/gitpod/ws-proxy/pkg/sshproxy"
41
)
42
43
// RouteHandlerConfig configures a RouteHandler.
44
type RouteHandlerConfig struct {
45
Config *Config
46
DefaultTransport http.RoundTripper
47
WorkspaceAuthHandler mux.MiddlewareFunc
48
}
49
50
// RouteHandlerConfigOpt modifies the router handler config.
51
type RouteHandlerConfigOpt func(*Config, *RouteHandlerConfig)
52
53
// WithDefaultAuth enables workspace access authentication.
54
func WithDefaultAuth(infoprov common.WorkspaceInfoProvider) RouteHandlerConfigOpt {
55
return func(config *Config, c *RouteHandlerConfig) {
56
c.WorkspaceAuthHandler = WorkspaceAuthHandler(config.GitpodInstallation.HostName, infoprov)
57
}
58
}
59
60
// NewRouteHandlerConfig creates a new instance.
61
func NewRouteHandlerConfig(config *Config, opts ...RouteHandlerConfigOpt) (*RouteHandlerConfig, error) {
62
cfg := &RouteHandlerConfig{
63
Config: config,
64
DefaultTransport: createDefaultTransport(config.TransportConfig),
65
WorkspaceAuthHandler: func(h http.Handler) http.Handler { return h },
66
}
67
for _, o := range opts {
68
o(config, cfg)
69
}
70
return cfg, nil
71
}
72
73
// RouteHandler is a function that handles a HTTP route.
74
type RouteHandler = func(r *mux.Router, config *RouteHandlerConfig)
75
76
// installWorkspaceRoutes configures routing of workspace and IDE requests.
77
func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip common.WorkspaceInfoProvider, sshGatewayServer *sshproxy.Server) error {
78
r.Use(logHandler)
79
r.Use(instrumentServerMetrics)
80
81
// Note: the order of routes defines their priority.
82
// Routes registered first have priority over those that come afterwards.
83
routes := newIDERoutes(config, ip)
84
85
// if sshGatewayServer not nil, we use /_ssh/host_keys to provider public host key
86
if sshGatewayServer != nil {
87
routes.HandleSSHHostKeyRoute(r.Path("/_ssh/host_keys"), sshGatewayServer.HostKeys)
88
routes.HandleSSHOverWebsocketTunnel(r.Path("/_ssh/tunnel"), sshGatewayServer)
89
90
// This is for backward compatibility.
91
routes.HandleSSHOverWebsocketTunnel(r.Path("/_supervisor/tunnel/ssh"), sshGatewayServer)
92
routes.HandleCreateKeyRoute(r.Path("/_supervisor/v1/ssh_keys/create"), sshGatewayServer.HostKeys)
93
}
94
95
// The favicon warants special handling, because we pull that from the supervisor frontend
96
// rather than the IDE.
97
faviconRouter := r.Path("/favicon.ico").Subrouter()
98
faviconRouter.Use(handlers.CompressHandler)
99
faviconRouter.Use(func(h http.Handler) http.Handler {
100
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
101
req.URL.Path = "/_supervisor/frontend/favicon.ico"
102
h.ServeHTTP(resp, req)
103
})
104
})
105
routes.HandleSupervisorFrontendRoute(faviconRouter.NewRoute())
106
107
routes.HandleDirectSupervisorRoute(enableCompression(r).PathPrefix("/_supervisor/frontend").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
108
return rm.Vars[common.DebugWorkspaceIdentifier] == "true"
109
}), false)
110
routes.HandleSupervisorFrontendRoute(enableCompression(r).PathPrefix("/_supervisor/frontend"))
111
112
statusErrorHandler := func(rw http.ResponseWriter, req *http.Request, connectErr error) {
113
log.Infof("status handler: could not connect to backend %s: %s", req.URL.String(), connectErrorToCause(connectErr))
114
115
rw.WriteHeader(http.StatusBadGateway)
116
}
117
118
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1/status/supervisor"), false, withErrorHandler(statusErrorHandler))
119
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1/status/ide"), false, withErrorHandler(statusErrorHandler))
120
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1/status/content"), true, withErrorHandler(statusErrorHandler))
121
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1"), true)
122
routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor"), true)
123
124
rootRouter := enableCompression(r)
125
rootRouter.Use(func(h http.Handler) http.Handler {
126
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
127
// This is just an alias to callback.html to make its purpose more explicit,
128
// it will be served by blobserve.
129
if req.URL.Path == "/vscode-extension-auth-callback" {
130
req.URL.Path = "/callback.html"
131
}
132
h.ServeHTTP(resp, req)
133
})
134
})
135
err := installDebugWorkspaceRoutes(rootRouter.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
136
return rm.Vars[common.DebugWorkspaceIdentifier] == "true"
137
}).Subrouter(), routes.Config, routes.InfoProvider)
138
if err != nil {
139
return err
140
}
141
routes.HandleRoot(rootRouter.NewRoute())
142
return nil
143
}
144
145
func enableCompression(r *mux.Router) *mux.Router {
146
res := r.NewRoute().Subrouter()
147
res.Use(handlers.CompressHandler)
148
return res
149
}
150
151
func newIDERoutes(config *RouteHandlerConfig, ip common.WorkspaceInfoProvider) *ideRoutes {
152
return &ideRoutes{
153
Config: config,
154
InfoProvider: ip,
155
workspaceMustExistHandler: workspaceMustExistHandler(config.Config, ip),
156
}
157
}
158
159
type ideRoutes struct {
160
Config *RouteHandlerConfig
161
InfoProvider common.WorkspaceInfoProvider
162
163
workspaceMustExistHandler mux.MiddlewareFunc
164
}
165
166
func (ir *ideRoutes) HandleSSHHostKeyRoute(route *mux.Route, hostKeyList []ssh.Signer) {
167
shk := make([]struct {
168
Type string `json:"type"`
169
HostKey string `json:"host_key"`
170
}, len(hostKeyList))
171
for i, hk := range hostKeyList {
172
shk[i].Type = hk.PublicKey().Type()
173
shk[i].HostKey = base64.StdEncoding.EncodeToString(hk.PublicKey().Marshal())
174
}
175
byt, err := json.Marshal(shk)
176
if err != nil {
177
log.WithError(err).Error("ssh_host_key router setup failed")
178
return
179
}
180
r := route.Subrouter()
181
r.Use(logRouteHandlerHandler("HandleSSHHostKeyRoute"))
182
r.NewRoute().HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
183
rw.Header().Add("Content-Type", "application/json")
184
rw.Write(byt)
185
}).Name("ssh_host_key")
186
}
187
188
func (ir *ideRoutes) HandleCreateKeyRoute(route *mux.Route, hostKeyList []ssh.Signer) {
189
r := route.Subrouter()
190
r.Use(logRouteHandlerHandler("HandleCreateKeyRoute"))
191
192
r.Use(ir.workspaceMustExistHandler)
193
r.Use(ir.Config.WorkspaceAuthHandler)
194
195
r.NewRoute().HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
196
resp := struct {
197
Privatekey string `json:"privateKey"`
198
UserName string `json:"userName"`
199
HostKey struct {
200
Type string `json:"type"`
201
Value string `json:"value"`
202
} `json:"hostKey"`
203
}{}
204
205
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
206
if err != nil {
207
log.WithError(err).Error("failed to generate key")
208
return
209
}
210
211
block, err := ssh.MarshalPrivateKey(privateKey, "")
212
if err != nil {
213
log.WithError(err).Error("failed to marshal key")
214
return
215
}
216
resp.Privatekey = string(pem.EncodeToMemory(block))
217
resp.UserName = "gitpod"
218
219
var hostKey ssh.Signer
220
for _, hk := range hostKeyList {
221
if hk.PublicKey().Type() != ssh.KeyAlgoRSA {
222
hostKey = hk
223
break
224
}
225
if hostKey == nil {
226
hostKey = hk
227
}
228
}
229
resp.HostKey.Type = hostKey.PublicKey().Type()
230
resp.HostKey.Value = base64.StdEncoding.EncodeToString(hostKey.PublicKey().Marshal())
231
byt, err := json.Marshal(resp)
232
if err != nil {
233
log.WithError(err).Error("cannot marshal response")
234
return
235
}
236
w.Header().Add("Content-Type", "application/json")
237
w.Write(byt)
238
})
239
}
240
241
var websocketCloseErrorPattern = regexp.MustCompile(`websocket: close (\d+)`)
242
243
func extractCloseErrorCode(errStr string) string {
244
matches := websocketCloseErrorPattern.FindStringSubmatch(errStr)
245
if len(matches) < 2 {
246
return "unknown"
247
}
248
249
return matches[1]
250
}
251
252
func (ir *ideRoutes) HandleSSHOverWebsocketTunnel(route *mux.Route, sshGatewayServer *sshproxy.Server) {
253
r := route.Subrouter()
254
r.Use(logRouteHandlerHandler("HandleSSHOverWebsocketTunnel"))
255
r.Use(ir.workspaceMustExistHandler)
256
r.Use(ir.Config.WorkspaceAuthHandler)
257
258
r.NewRoute().HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
259
var err error
260
sshproxy.SSHTunnelOpenedTotal.WithLabelValues().Inc()
261
defer func() {
262
code := "unknown"
263
if err != nil {
264
code = extractCloseErrorCode(err.Error())
265
}
266
sshproxy.SSHTunnelClosedTotal.WithLabelValues(code).Inc()
267
}()
268
startTime := time.Now()
269
log := log.WithField("userAgent", r.Header.Get("user-agent")).WithField("remoteAddr", r.RemoteAddr)
270
271
upgrader := websocket.Upgrader{}
272
wsConn, err := upgrader.Upgrade(w, r, nil)
273
if err != nil {
274
log.WithError(err).Error("tunnel ssh: upgrade to the WebSocket protocol failed")
275
return
276
}
277
coords := getWorkspaceCoords(r)
278
infomap := make(map[string]string)
279
infomap[common.WorkspaceIDIdentifier] = coords.ID
280
infomap[common.DebugWorkspaceIdentifier] = strconv.FormatBool(coords.Debug)
281
ctx := context.WithValue(r.Context(), common.WorkspaceInfoIdentifier, infomap)
282
conn, err := gitpod.NewWebsocketConnection(ctx, wsConn, func(staleErr error) {
283
log.WithError(staleErr).Error("tunnel ssh: closing stale connection")
284
})
285
if err != nil {
286
log.WithError(err).Error("tunnel ssh: upgrade to the WebSocket protocol failed")
287
return
288
}
289
log.Debugf("tunnel ssh: Connected from %s", conn.RemoteAddr())
290
sshGatewayServer.HandleConn(conn)
291
log.WithField("duration", time.Since(startTime).Seconds()).Debugf("tunnel ssh: Disconnect from %s", conn.RemoteAddr())
292
})
293
}
294
295
func (ir *ideRoutes) HandleDirectSupervisorRoute(route *mux.Route, authenticated bool, proxyPassOpts ...proxyPassOpt) {
296
r := route.Subrouter()
297
r.Use(logRouteHandlerHandler(fmt.Sprintf("HandleDirectSupervisorRoute (authenticated: %v)", authenticated)))
298
r.Use(ir.workspaceMustExistHandler)
299
if authenticated {
300
r.Use(ir.Config.WorkspaceAuthHandler)
301
}
302
303
r.NewRoute().HandlerFunc(proxyPass(ir.Config, ir.InfoProvider, workspacePodSupervisorResolver, proxyPassOpts...)).Name("supervisor")
304
}
305
306
func (ir *ideRoutes) HandleSupervisorFrontendRoute(route *mux.Route) {
307
if ir.Config.Config.BlobServer == nil {
308
// if we don't have blobserve, we serve the supervisor frontend from supervisor directly
309
ir.HandleDirectSupervisorRoute(route, false)
310
return
311
}
312
313
r := route.Subrouter()
314
r.Use(logRouteHandlerHandler("SupervisorIDEHostHandler"))
315
r.Use(ir.workspaceMustExistHandler)
316
// strip the frontend prefix, just for good measure
317
r.Use(func(h http.Handler) http.Handler {
318
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
319
req.URL.Path = strings.TrimPrefix(req.URL.Path, "/_supervisor/frontend")
320
h.ServeHTTP(resp, req)
321
})
322
})
323
// always hit the blobserver to ensure that blob is downloaded
324
r.NewRoute().HandlerFunc(proxyPass(ir.Config, ir.InfoProvider, func(cfg *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (*url.URL, string, error) {
325
info := getWorkspaceInfoFromContext(req.Context())
326
return resolveSupervisorURL(cfg, info, req)
327
}, func(h *proxyPassConfig) {
328
h.Transport = &blobserveTransport{
329
transport: h.Transport,
330
Config: ir.Config.Config,
331
resolveImage: func(t *blobserveTransport, req *http.Request) string {
332
info := getWorkspaceInfoFromContext(req.Context())
333
if info == nil && len(ir.Config.Config.WorkspacePodConfig.SupervisorImage) == 0 {
334
// no workspace information available - cannot resolve supervisor image
335
return ""
336
}
337
338
// use the config value for backwards compatibility when info.SupervisorImage is not set
339
image := ir.Config.Config.WorkspacePodConfig.SupervisorImage
340
if info != nil && len(info.SupervisorImage) > 0 {
341
image = info.SupervisorImage
342
}
343
344
path := strings.TrimPrefix(req.URL.Path, "/"+image)
345
if path == "/worker-proxy.js" {
346
// worker must be served from the same origin
347
return ""
348
}
349
return image
350
},
351
}
352
}, withUseTargetHost())).Name("supervisor_frontend")
353
}
354
355
func resolveSupervisorURL(cfg *Config, info *common.WorkspaceInfo, req *http.Request) (*url.URL, string, error) {
356
if info == nil && len(cfg.WorkspacePodConfig.SupervisorImage) == 0 {
357
log.WithFields(log.OWI("", getWorkspaceCoords(req).ID, "")).Warn("no workspace info available - cannot resolve supervisor route")
358
return nil, "", xerrors.Errorf("no workspace information available - cannot resolve supervisor route")
359
}
360
361
// use the config value for backwards compatibility when info.SupervisorImage is not set
362
supervisorImage := cfg.WorkspacePodConfig.SupervisorImage
363
if info != nil && len(info.SupervisorImage) > 0 {
364
supervisorImage = info.SupervisorImage
365
}
366
367
var dst url.URL
368
dst.Scheme = cfg.BlobServer.Scheme
369
dst.Host = cfg.BlobServer.Host
370
dst.Path = cfg.BlobServer.PathPrefix + "/" + supervisorImage
371
return &dst, "blobserve/supervisor", nil
372
}
373
374
type BlobserveInlineVars struct {
375
IDE string `json:"ide"`
376
SupervisorImage string `json:"supervisor"`
377
}
378
379
func (ir *ideRoutes) HandleRoot(route *mux.Route) {
380
r := route.Subrouter()
381
r.Use(logRouteHandlerHandler("handleRoot"))
382
r.Use(ir.workspaceMustExistHandler)
383
384
proxyPassWoSensitiveCookies := sensitiveCookieHandler(ir.Config.Config.GitpodInstallation.HostName)(proxyPass(ir.Config, ir.InfoProvider, workspacePodResolver))
385
directIDEPass := ir.Config.WorkspaceAuthHandler(proxyPassWoSensitiveCookies)
386
387
// always hit the blobserver to ensure that blob is downloaded
388
r.NewRoute().HandlerFunc(proxyPass(ir.Config, ir.InfoProvider, dynamicIDEResolver, func(h *proxyPassConfig) {
389
h.Transport = &blobserveTransport{
390
transport: h.Transport,
391
Config: ir.Config.Config,
392
resolveImage: func(t *blobserveTransport, req *http.Request) string {
393
info := getWorkspaceInfoFromContext(req.Context())
394
if info == nil {
395
// no workspace information available - cannot resolve IDE image and path
396
return ""
397
}
398
image := info.IDEImage
399
imagePath := strings.TrimPrefix(req.URL.Path, t.Config.BlobServer.PathPrefix+"/"+image)
400
if imagePath != "/index.html" && imagePath != "/" {
401
return image
402
}
403
// blobserve can inline static links in index.html for IDE and supervisor to avoid redirects for each supervisor resource
404
// but it has to know exposed URLs in the context of current workspace cluster
405
// so first we ask blobserve to preload the supervisor image
406
// and if it is successful we pass exposed URLs to IDE and supervisor to blobserve for inlining
407
supervisorURL, supervisorResource, err := resolveSupervisorURL(t.Config, info, req)
408
if err != nil {
409
log.WithError(err).Error("could not preload supervisor")
410
return image
411
}
412
supervisorURLString := supervisorURL.String() + "/main.js"
413
preloadSupervisorReq, err := http.NewRequest("HEAD", supervisorURLString, nil)
414
if err != nil {
415
log.WithField("supervisorURL", supervisorURL).WithError(err).Error("could not preload supervisor")
416
return image
417
}
418
preloadSupervisorReq = withResourceMetricsLabel(preloadSupervisorReq, supervisorResource)
419
preloadSupervisorReq = withHttpVersionMetricsLabel(preloadSupervisorReq)
420
resp, err := t.DoRoundTrip(preloadSupervisorReq)
421
if err != nil {
422
log.WithField("supervisorURL", supervisorURL).WithError(err).Error("could not preload supervisor")
423
return image
424
}
425
_ = resp.Body.Close()
426
if resp.StatusCode != http.StatusOK {
427
log.WithField("supervisorURL", supervisorURL).WithField("statusCode", resp.StatusCode).WithField("status", resp.Status).Error("could not preload supervisor")
428
return image
429
}
430
431
// use the config value for backwards compatibility when info.SupervisorImage is not set
432
supervisorImage := t.Config.WorkspacePodConfig.SupervisorImage
433
if len(info.SupervisorImage) > 0 {
434
supervisorImage = info.SupervisorImage
435
}
436
437
inlineVars := &BlobserveInlineVars{
438
IDE: t.asBlobserveURL(image, ""),
439
SupervisorImage: t.asBlobserveURL(supervisorImage, ""),
440
}
441
inlinveVarsValue, err := json.Marshal(inlineVars)
442
if err != nil {
443
log.WithError(err).WithField("inlineVars", inlineVars).Error("could no serialize inline vars")
444
return image
445
}
446
447
req.Header.Add("X-BlobServe-InlineVars", string(inlinveVarsValue))
448
return image
449
},
450
}
451
}, withHTTPErrorHandler(directIDEPass), withUseTargetHost())).Name("root")
452
}
453
454
func installForeignRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) error {
455
r.Use(instrumentServerMetrics)
456
457
err := installWorkspacePortRoutes(r.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
458
workspacePathPrefix := rm.Vars[common.WorkspacePathPrefixIdentifier]
459
if workspacePathPrefix == "" || rm.Vars[common.WorkspacePortIdentifier] == "" {
460
return false
461
}
462
r.URL.Path = strings.TrimPrefix(r.URL.Path, workspacePathPrefix)
463
return true
464
}).Subrouter(), config, infoProvider)
465
if err != nil {
466
return err
467
}
468
err = installDebugWorkspaceRoutes(r.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
469
workspacePathPrefix := rm.Vars[common.WorkspacePathPrefixIdentifier]
470
if workspacePathPrefix == "" || rm.Vars[common.DebugWorkspaceIdentifier] != "true" {
471
return false
472
}
473
r.URL.Path = strings.TrimPrefix(r.URL.Path, workspacePathPrefix)
474
return true
475
}).Subrouter(), config, infoProvider)
476
if err != nil {
477
return err
478
}
479
installForeignBlobserveRoutes(r.NewRoute().Subrouter(), config, infoProvider)
480
return nil
481
}
482
483
const imagePathSeparator = "/__files__"
484
485
// installForeignBlobserveRoutes implements long-lived caching with versioned URLs, see https://web.dev/http-cache/#versioned-urls
486
func installForeignBlobserveRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) {
487
r.Use(logHandler)
488
r.Use(logRouteHandlerHandler("BlobserveRootHandler"))
489
490
// filter all session cookies
491
r.Use(sensitiveCookieHandler(config.Config.GitpodInstallation.HostName))
492
493
targetResolver := func(cfg *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (tgt *url.URL, str string, err error) {
494
segments := strings.SplitN(req.URL.Path, imagePathSeparator, 2)
495
if len(segments) < 2 {
496
return nil, "", xerrors.Errorf("invalid URL")
497
}
498
image, path := segments[0], segments[1]
499
500
req.URL.Path = path
501
502
var dst url.URL
503
dst.Scheme = cfg.BlobServer.Scheme
504
dst.Host = cfg.BlobServer.Host
505
dst.Path = cfg.BlobServer.PathPrefix + "/" + strings.TrimPrefix(image, "/")
506
return &dst, "blobserve/foreign_content", nil
507
}
508
r.NewRoute().Handler(proxyPass(config, infoProvider, targetResolver, withLongTermCaching(), withUseTargetHost())).Name("blobserve")
509
}
510
511
// installDebugWorkspaceRoutes configures for debug workspace.
512
func installDebugWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) error {
513
showPortNotFoundPage, err := servePortNotFoundPage(config.Config)
514
if err != nil {
515
return err
516
}
517
518
r.Use(logHandler)
519
r.Use(config.WorkspaceAuthHandler)
520
// filter all session cookies
521
r.Use(sensitiveCookieHandler(config.Config.GitpodInstallation.HostName))
522
523
r.NewRoute().HandlerFunc(proxyPass(config, infoProvider, workspacePodResolver, withHTTPErrorHandler(showPortNotFoundPage)))
524
return nil
525
}
526
527
// installWorkspacePortRoutes configures routing for exposed ports.
528
func installWorkspacePortRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) error {
529
showPortNotFoundPage, err := servePortNotFoundPage(config.Config)
530
if err != nil {
531
return err
532
}
533
534
portTransport := createDefaultTransport(config.Config.TransportConfig, withSkipTLSVerify())
535
536
r.Use(logHandler)
537
r.Use(config.WorkspaceAuthHandler)
538
// filter all session cookies
539
r.Use(sensitiveCookieHandler(config.Config.GitpodInstallation.HostName))
540
541
// forward request to workspace port
542
r.NewRoute().HandlerFunc(
543
func(rw http.ResponseWriter, r *http.Request) {
544
// a work-around for servers which does not respect case-insensitive headers, see https://github.com/gitpod-io/gitpod/issues/4047#issuecomment-856566526
545
for _, name := range []string{"Key", "Extensions", "Accept", "Protocol", "Version"} {
546
values := r.Header["Sec-Websocket-"+name]
547
if len(values) != 0 {
548
r.Header.Del("Sec-Websocket-" + name)
549
r.Header["Sec-WebSocket-"+name] = values
550
}
551
}
552
r.Header.Add("X-Forwarded-Proto", "https")
553
r.Header.Add("X-Forwarded-Host", r.Host)
554
r.Header.Add("X-Forwarded-Port", "443")
555
556
coords := getWorkspaceCoords(r)
557
if coords.Debug {
558
r.Header.Add("X-WS-Proxy-Debug-Port", coords.Port)
559
}
560
561
proxyPass(
562
config,
563
infoProvider,
564
workspacePodPortResolver,
565
withHTTPErrorHandler(showPortNotFoundPage),
566
withXFrameOptionsFilter(),
567
func(h *proxyPassConfig) {
568
h.Transport = portTransport
569
},
570
)(rw, r)
571
},
572
)
573
574
return nil
575
}
576
577
// workspacePodResolver resolves to the workspace pod's url from the given request.
578
func workspacePodResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (url *url.URL, resource string, err error) {
579
coords := getWorkspaceCoords(req)
580
var port string
581
if coords.Debug {
582
resource = "debug_workspace"
583
port = fmt.Sprint(config.WorkspacePodConfig.IDEDebugPort)
584
} else {
585
resource = "workspace"
586
port = fmt.Sprint(config.WorkspacePodConfig.TheiaPort)
587
}
588
workspaceInfo := infoProvider.WorkspaceInfo(coords.ID)
589
url, err = buildWorkspacePodURL(api.PortProtocol_PORT_PROTOCOL_HTTP, workspaceInfo.IPAddress, port)
590
return
591
}
592
593
// workspacePodPortResolver resolves to the workspace pods ports.
594
func workspacePodPortResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (url *url.URL, resource string, err error) {
595
coords := getWorkspaceCoords(req)
596
workspaceInfo := infoProvider.WorkspaceInfo(coords.ID)
597
var port string
598
protocol := api.PortProtocol_PORT_PROTOCOL_HTTP
599
if coords.Debug {
600
resource = "debug_workspace_port"
601
port = fmt.Sprint(config.WorkspacePodConfig.DebugWorkspaceProxyPort)
602
} else {
603
resource = "workspace_port"
604
port = coords.Port
605
prt, err := strconv.ParseUint(port, 10, 16)
606
if err != nil {
607
log.WithField("port", port).WithError(err).Error("cannot convert port to int")
608
} else {
609
for _, p := range workspaceInfo.Ports {
610
if p.Port == uint32(prt) {
611
protocol = p.Protocol
612
break
613
}
614
}
615
}
616
}
617
url, err = buildWorkspacePodURL(protocol, workspaceInfo.IPAddress, port)
618
return
619
}
620
621
// workspacePodSupervisorResolver resolves to the workspace pods Supervisor url from the given request.
622
func workspacePodSupervisorResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (url *url.URL, resource string, err error) {
623
coords := getWorkspaceCoords(req)
624
var port string
625
if coords.Debug {
626
resource = "debug_workspace/supervisor"
627
port = fmt.Sprint(config.WorkspacePodConfig.SupervisorDebugPort)
628
} else {
629
resource = "workspace/supervisor"
630
port = fmt.Sprint(config.WorkspacePodConfig.SupervisorPort)
631
}
632
workspaceInfo := infoProvider.WorkspaceInfo(coords.ID)
633
url, err = buildWorkspacePodURL(api.PortProtocol_PORT_PROTOCOL_HTTP, workspaceInfo.IPAddress, port)
634
return
635
}
636
637
func dynamicIDEResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (res *url.URL, resource string, err error) {
638
info := getWorkspaceInfoFromContext(req.Context())
639
if info == nil {
640
log.WithFields(log.OWI("", getWorkspaceCoords(req).ID, "")).Warn("no workspace info available - cannot resolve Theia route")
641
return nil, "", xerrors.Errorf("no workspace information available - cannot resolve Theia route")
642
}
643
644
var dst url.URL
645
dst.Scheme = config.BlobServer.Scheme
646
dst.Host = config.BlobServer.Host
647
dst.Path = config.BlobServer.PathPrefix + "/" + info.IDEImage
648
649
return &dst, "blobserve/ide", nil
650
}
651
652
func buildWorkspacePodURL(protocol api.PortProtocol, ipAddress string, port string) (*url.URL, error) {
653
portProtocol := ""
654
switch protocol {
655
case api.PortProtocol_PORT_PROTOCOL_HTTP:
656
portProtocol = "http"
657
case api.PortProtocol_PORT_PROTOCOL_HTTPS:
658
portProtocol = "https"
659
default:
660
return nil, xerrors.Errorf("protocol not supported")
661
}
662
return url.Parse(fmt.Sprintf("%v://%v:%v", portProtocol, ipAddress, port))
663
}
664
665
type wsproxyContextKey struct{}
666
667
var (
668
logContextValueKey = wsproxyContextKey{}
669
infoContextValueKey = wsproxyContextKey{}
670
)
671
672
func logHandler(h http.Handler) http.Handler {
673
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
674
var (
675
vars = mux.Vars(req)
676
wsID = vars[common.WorkspaceIDIdentifier]
677
port = vars[common.WorkspacePortIdentifier]
678
)
679
entry := logrus.Fields{
680
"workspaceId": wsID,
681
"portID": port,
682
"url": req.URL.String(),
683
}
684
ctx := context.WithValue(req.Context(), logContextValueKey, entry)
685
req = req.WithContext(ctx)
686
687
h.ServeHTTP(resp, req)
688
})
689
}
690
691
func logRouteHandlerHandler(routeHandlerName string) mux.MiddlewareFunc {
692
return func(h http.Handler) http.Handler {
693
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
694
getLog(req.Context()).WithField("routeHandler", routeHandlerName).Debug("hit route handler")
695
h.ServeHTTP(resp, req)
696
})
697
}
698
}
699
700
func getLog(ctx context.Context) *logrus.Entry {
701
r := ctx.Value(logContextValueKey)
702
rl, ok := r.(logrus.Fields)
703
if rl == nil || !ok {
704
return log.Log
705
}
706
707
return log.WithFields(rl)
708
}
709
710
func sensitiveCookieHandler(domain string) func(h http.Handler) http.Handler {
711
return func(h http.Handler) http.Handler {
712
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
713
cookies := removeSensitiveCookies(readCookies(req.Header, ""), domain)
714
header := make([]string, 0, len(cookies))
715
for _, c := range cookies {
716
if c == nil {
717
continue
718
}
719
720
cookie := c.String()
721
if cookie == "" {
722
// because we're checking for nil above, it must be that the cookie name is invalid.
723
// Some languages have no quarels with producing invalid cookie names, so we must too.
724
// See https://github.com/gitpod-io/gitpod/issues/2470 for more details.
725
var (
726
originalName = c.Name
727
replacementName = fmt.Sprintf("name%d%d", rand.Uint64(), time.Now().Unix())
728
)
729
c.Name = replacementName
730
cookie = c.String()
731
if cookie == "" {
732
// despite our best efforts, we still couldn't render the cookie. We'll just drop
733
// it at this point
734
continue
735
}
736
737
cookie = strings.Replace(cookie, replacementName, originalName, 1)
738
c.Name = originalName
739
}
740
741
header = append(header, cookie)
742
}
743
744
// using the header string slice here directly would result in multiple cookie header
745
// being sent. See https://github.com/gitpod-io/gitpod/issues/2121.
746
req.Header["Cookie"] = []string{strings.Join(header, ";")}
747
748
h.ServeHTTP(resp, req)
749
})
750
}
751
}
752
753
// workspaceMustExistHandler redirects if we don't know about a workspace yet.
754
func workspaceMustExistHandler(config *Config, infoProvider common.WorkspaceInfoProvider) mux.MiddlewareFunc {
755
return func(h http.Handler) http.Handler {
756
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
757
coords := getWorkspaceCoords(req)
758
info := infoProvider.WorkspaceInfo(coords.ID)
759
if info == nil {
760
redirectURL := fmt.Sprintf("%s://%s/start/?not_found=true#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, coords.ID)
761
http.Redirect(resp, req, redirectURL, http.StatusFound)
762
return
763
}
764
765
h.ServeHTTP(resp, req.WithContext(context.WithValue(req.Context(), infoContextValueKey, info)))
766
})
767
}
768
}
769
770
// getWorkspaceInfoFromContext retrieves workspace information put there by the workspaceMustExistHandler.
771
func getWorkspaceInfoFromContext(ctx context.Context) *common.WorkspaceInfo {
772
r := ctx.Value(infoContextValueKey)
773
rl, ok := r.(*common.WorkspaceInfo)
774
if !ok {
775
return nil
776
}
777
return rl
778
}
779
780
// removeSensitiveCookies all sensitive cookies from the list.
781
// This function modifies the slice in-place.
782
func removeSensitiveCookies(cookies []*http.Cookie, domain string) []*http.Cookie {
783
hostnamePrefix := domain
784
for _, c := range []string{" ", "-", "."} {
785
hostnamePrefix = strings.ReplaceAll(hostnamePrefix, c, "_")
786
}
787
hostnamePrefix = "_" + hostnamePrefix + "_"
788
789
n := 0
790
for _, c := range cookies {
791
if strings.HasPrefix(c.Name, hostnamePrefix) || strings.HasPrefix(c.Name, "__Host-"+hostnamePrefix) {
792
// skip session cookies
793
continue
794
}
795
log.WithField("hostnamePrefix", hostnamePrefix).WithField("name", c.Name).Debug("keeping cookie")
796
cookies[n] = c
797
n++
798
}
799
return cookies[:n]
800
}
801
802
// region blobserve transport.
803
type blobserveTransport struct {
804
transport http.RoundTripper
805
Config *Config
806
resolveImage func(t *blobserveTransport, req *http.Request) string
807
}
808
809
func (t *blobserveTransport) DoRoundTrip(req *http.Request) (resp *http.Response, err error) {
810
for i := 0; i < 5; i++ {
811
resp, err = t.transport.RoundTrip(req)
812
if err != nil {
813
return nil, err
814
}
815
816
if resp.StatusCode >= http.StatusBadRequest {
817
respBody, err := io.ReadAll(resp.Body)
818
if err != nil {
819
return nil, err
820
}
821
_ = resp.Body.Close()
822
823
if resp.StatusCode == http.StatusServiceUnavailable && string(respBody) == "timeout" {
824
// on timeout try again till the client request is cancelled
825
// blob server sometimes takes time to pull a new image
826
continue
827
}
828
829
// treat any client or server error code as a http error
830
return nil, xerrors.Errorf("blobserver error: (%d) %s", resp.StatusCode, string(respBody))
831
}
832
break
833
}
834
return resp, err
835
}
836
837
func isWebSocketUpgrade(req *http.Request) bool {
838
return strings.EqualFold(req.Header.Get("Upgrade"), "websocket") &&
839
strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade")
840
}
841
842
func (t *blobserveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
843
if isWebSocketUpgrade(req) {
844
return nil, xerrors.Errorf("blobserve: websocket not supported")
845
}
846
847
image := t.resolveImage(t, req)
848
849
resp, err = t.DoRoundTrip(req)
850
if err != nil {
851
return nil, err
852
}
853
854
if resp.StatusCode != http.StatusOK {
855
// only redirect successful responses
856
return resp, nil
857
}
858
859
if req.URL.RawQuery != "" {
860
// URLs with query cannot be static, i.e. the server is required to resolve the query
861
return resp, nil
862
}
863
864
// region use fetch metadata to avoid redirections https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header
865
mode := req.Header.Get("Sec-Fetch-Mode")
866
dest := req.Header.Get("Sec-Fetch-Dest")
867
if mode == "" && strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html") {
868
// fallback for user agents not supporting fetch metadata to avoid redirecting on user navigation
869
mode = "navigate"
870
}
871
if mode == "navigate" || mode == "nested-navigate" || mode == "websocket" {
872
// user navigation and websocket requests should not be redirected
873
return resp, nil
874
}
875
876
if mode == "same-origin" && !(dest == "worker" || dest == "sharedworker") {
877
// same origin should not be redirected, except workers
878
// supervisor installs the worker proxy from the workspace origin serving content from the blobserve origin
879
return resp, nil
880
}
881
// endregion
882
883
if image == "" {
884
return resp, nil
885
}
886
887
_ = resp.Body.Close()
888
return t.redirect(image, req)
889
}
890
891
func (t *blobserveTransport) redirect(image string, req *http.Request) (*http.Response, error) {
892
path := strings.TrimPrefix(req.URL.Path, t.Config.BlobServer.PathPrefix+"/"+image)
893
location := t.asBlobserveURL(image, path)
894
895
header := make(http.Header, 2)
896
header.Set("Location", location)
897
header.Set("Content-Type", "text/html; charset=utf-8")
898
899
code := http.StatusSeeOther
900
var (
901
status = http.StatusText(code)
902
content = []byte("<a href=\"" + location + "\">" + status + "</a>.\n\n")
903
)
904
905
return &http.Response{
906
Request: req,
907
Header: header,
908
Body: io.NopCloser(bytes.NewReader(content)),
909
ContentLength: int64(len(content)),
910
StatusCode: code,
911
Status: status,
912
}, nil
913
}
914
915
func (t *blobserveTransport) asBlobserveURL(image string, path string) string {
916
return fmt.Sprintf("%s://ide.%s/blobserve/%s%s%s",
917
t.Config.GitpodInstallation.Scheme,
918
t.Config.GitpodInstallation.HostName,
919
image,
920
imagePathSeparator,
921
path,
922
)
923
}
924
925
// endregion
926
927
const (
928
builtinPagePortNotFound = "port-not-found.html"
929
)
930
931
func servePortNotFoundPage(config *Config) (http.Handler, error) {
932
fn := filepath.Join(config.BuiltinPages.Location, builtinPagePortNotFound)
933
if tp := os.Getenv("TELEPRESENCE_ROOT"); tp != "" {
934
fn = filepath.Join(tp, fn)
935
}
936
page, err := os.ReadFile(fn)
937
if err != nil {
938
return nil, err
939
}
940
page = bytes.ReplaceAll(page, []byte("https://gitpod.io"), []byte(fmt.Sprintf("%s://%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName)))
941
942
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
943
w.WriteHeader(http.StatusNotFound)
944
_, _ = w.Write(page)
945
}), nil
946
}
947
948