Path: blob/main/components/ws-proxy/pkg/proxy/routes.go
2500 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package proxy56import (7"bytes"8"context"9"crypto/ecdsa"10"crypto/elliptic"11crand "crypto/rand"12"encoding/base64"13"encoding/json"14"encoding/pem"15"fmt"16"io"17"math/rand"18"net/http"19"net/url"20"os"21"path/filepath"22"regexp"23"strconv"24"strings"25"time"2627"github.com/gorilla/websocket"2829"github.com/gitpod-io/golang-crypto/ssh"30"github.com/gorilla/handlers"31"github.com/gorilla/mux"32"github.com/sirupsen/logrus"33"golang.org/x/xerrors"3435"github.com/gitpod-io/gitpod/common-go/log"36gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"37"github.com/gitpod-io/gitpod/ws-manager/api"38"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"39"github.com/gitpod-io/gitpod/ws-proxy/pkg/sshproxy"40)4142// RouteHandlerConfig configures a RouteHandler.43type RouteHandlerConfig struct {44Config *Config45DefaultTransport http.RoundTripper46WorkspaceAuthHandler mux.MiddlewareFunc47}4849// RouteHandlerConfigOpt modifies the router handler config.50type RouteHandlerConfigOpt func(*Config, *RouteHandlerConfig)5152// WithDefaultAuth enables workspace access authentication.53func WithDefaultAuth(infoprov common.WorkspaceInfoProvider) RouteHandlerConfigOpt {54return func(config *Config, c *RouteHandlerConfig) {55c.WorkspaceAuthHandler = WorkspaceAuthHandler(config.GitpodInstallation.HostName, infoprov)56}57}5859// NewRouteHandlerConfig creates a new instance.60func NewRouteHandlerConfig(config *Config, opts ...RouteHandlerConfigOpt) (*RouteHandlerConfig, error) {61cfg := &RouteHandlerConfig{62Config: config,63DefaultTransport: createDefaultTransport(config.TransportConfig),64WorkspaceAuthHandler: func(h http.Handler) http.Handler { return h },65}66for _, o := range opts {67o(config, cfg)68}69return cfg, nil70}7172// RouteHandler is a function that handles a HTTP route.73type RouteHandler = func(r *mux.Router, config *RouteHandlerConfig)7475// installWorkspaceRoutes configures routing of workspace and IDE requests.76func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip common.WorkspaceInfoProvider, sshGatewayServer *sshproxy.Server) error {77r.Use(logHandler)78r.Use(instrumentServerMetrics)7980// Note: the order of routes defines their priority.81// Routes registered first have priority over those that come afterwards.82routes := newIDERoutes(config, ip)8384// if sshGatewayServer not nil, we use /_ssh/host_keys to provider public host key85if sshGatewayServer != nil {86routes.HandleSSHHostKeyRoute(r.Path("/_ssh/host_keys"), sshGatewayServer.HostKeys)87routes.HandleSSHOverWebsocketTunnel(r.Path("/_ssh/tunnel"), sshGatewayServer)8889// This is for backward compatibility.90routes.HandleSSHOverWebsocketTunnel(r.Path("/_supervisor/tunnel/ssh"), sshGatewayServer)91routes.HandleCreateKeyRoute(r.Path("/_supervisor/v1/ssh_keys/create"), sshGatewayServer.HostKeys)92}9394// The favicon warants special handling, because we pull that from the supervisor frontend95// rather than the IDE.96faviconRouter := r.Path("/favicon.ico").Subrouter()97faviconRouter.Use(handlers.CompressHandler)98faviconRouter.Use(func(h http.Handler) http.Handler {99return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {100req.URL.Path = "/_supervisor/frontend/favicon.ico"101h.ServeHTTP(resp, req)102})103})104routes.HandleSupervisorFrontendRoute(faviconRouter.NewRoute())105106routes.HandleDirectSupervisorRoute(enableCompression(r).PathPrefix("/_supervisor/frontend").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {107return rm.Vars[common.DebugWorkspaceIdentifier] == "true"108}), false)109routes.HandleSupervisorFrontendRoute(enableCompression(r).PathPrefix("/_supervisor/frontend"))110111statusErrorHandler := func(rw http.ResponseWriter, req *http.Request, connectErr error) {112log.Infof("status handler: could not connect to backend %s: %s", req.URL.String(), connectErrorToCause(connectErr))113114rw.WriteHeader(http.StatusBadGateway)115}116117routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1/status/supervisor"), false, withErrorHandler(statusErrorHandler))118routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1/status/ide"), false, withErrorHandler(statusErrorHandler))119routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1/status/content"), true, withErrorHandler(statusErrorHandler))120routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor/v1"), true)121routes.HandleDirectSupervisorRoute(r.PathPrefix("/_supervisor"), true)122123rootRouter := enableCompression(r)124rootRouter.Use(func(h http.Handler) http.Handler {125return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {126// This is just an alias to callback.html to make its purpose more explicit,127// it will be served by blobserve.128if req.URL.Path == "/vscode-extension-auth-callback" {129req.URL.Path = "/callback.html"130}131h.ServeHTTP(resp, req)132})133})134err := installDebugWorkspaceRoutes(rootRouter.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {135return rm.Vars[common.DebugWorkspaceIdentifier] == "true"136}).Subrouter(), routes.Config, routes.InfoProvider)137if err != nil {138return err139}140routes.HandleRoot(rootRouter.NewRoute())141return nil142}143144func enableCompression(r *mux.Router) *mux.Router {145res := r.NewRoute().Subrouter()146res.Use(handlers.CompressHandler)147return res148}149150func newIDERoutes(config *RouteHandlerConfig, ip common.WorkspaceInfoProvider) *ideRoutes {151return &ideRoutes{152Config: config,153InfoProvider: ip,154workspaceMustExistHandler: workspaceMustExistHandler(config.Config, ip),155}156}157158type ideRoutes struct {159Config *RouteHandlerConfig160InfoProvider common.WorkspaceInfoProvider161162workspaceMustExistHandler mux.MiddlewareFunc163}164165func (ir *ideRoutes) HandleSSHHostKeyRoute(route *mux.Route, hostKeyList []ssh.Signer) {166shk := make([]struct {167Type string `json:"type"`168HostKey string `json:"host_key"`169}, len(hostKeyList))170for i, hk := range hostKeyList {171shk[i].Type = hk.PublicKey().Type()172shk[i].HostKey = base64.StdEncoding.EncodeToString(hk.PublicKey().Marshal())173}174byt, err := json.Marshal(shk)175if err != nil {176log.WithError(err).Error("ssh_host_key router setup failed")177return178}179r := route.Subrouter()180r.Use(logRouteHandlerHandler("HandleSSHHostKeyRoute"))181r.NewRoute().HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {182rw.Header().Add("Content-Type", "application/json")183rw.Write(byt)184}).Name("ssh_host_key")185}186187func (ir *ideRoutes) HandleCreateKeyRoute(route *mux.Route, hostKeyList []ssh.Signer) {188r := route.Subrouter()189r.Use(logRouteHandlerHandler("HandleCreateKeyRoute"))190191r.Use(ir.workspaceMustExistHandler)192r.Use(ir.Config.WorkspaceAuthHandler)193194r.NewRoute().HandlerFunc(func(w http.ResponseWriter, r *http.Request) {195resp := struct {196Privatekey string `json:"privateKey"`197UserName string `json:"userName"`198HostKey struct {199Type string `json:"type"`200Value string `json:"value"`201} `json:"hostKey"`202}{}203204privateKey, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)205if err != nil {206log.WithError(err).Error("failed to generate key")207return208}209210block, err := ssh.MarshalPrivateKey(privateKey, "")211if err != nil {212log.WithError(err).Error("failed to marshal key")213return214}215resp.Privatekey = string(pem.EncodeToMemory(block))216resp.UserName = "gitpod"217218var hostKey ssh.Signer219for _, hk := range hostKeyList {220if hk.PublicKey().Type() != ssh.KeyAlgoRSA {221hostKey = hk222break223}224if hostKey == nil {225hostKey = hk226}227}228resp.HostKey.Type = hostKey.PublicKey().Type()229resp.HostKey.Value = base64.StdEncoding.EncodeToString(hostKey.PublicKey().Marshal())230byt, err := json.Marshal(resp)231if err != nil {232log.WithError(err).Error("cannot marshal response")233return234}235w.Header().Add("Content-Type", "application/json")236w.Write(byt)237})238}239240var websocketCloseErrorPattern = regexp.MustCompile(`websocket: close (\d+)`)241242func extractCloseErrorCode(errStr string) string {243matches := websocketCloseErrorPattern.FindStringSubmatch(errStr)244if len(matches) < 2 {245return "unknown"246}247248return matches[1]249}250251func (ir *ideRoutes) HandleSSHOverWebsocketTunnel(route *mux.Route, sshGatewayServer *sshproxy.Server) {252r := route.Subrouter()253r.Use(logRouteHandlerHandler("HandleSSHOverWebsocketTunnel"))254r.Use(ir.workspaceMustExistHandler)255r.Use(ir.Config.WorkspaceAuthHandler)256257r.NewRoute().HandlerFunc(func(w http.ResponseWriter, r *http.Request) {258var err error259sshproxy.SSHTunnelOpenedTotal.WithLabelValues().Inc()260defer func() {261code := "unknown"262if err != nil {263code = extractCloseErrorCode(err.Error())264}265sshproxy.SSHTunnelClosedTotal.WithLabelValues(code).Inc()266}()267startTime := time.Now()268log := log.WithField("userAgent", r.Header.Get("user-agent")).WithField("remoteAddr", r.RemoteAddr)269270upgrader := websocket.Upgrader{}271wsConn, err := upgrader.Upgrade(w, r, nil)272if err != nil {273log.WithError(err).Error("tunnel ssh: upgrade to the WebSocket protocol failed")274return275}276coords := getWorkspaceCoords(r)277infomap := make(map[string]string)278infomap[common.WorkspaceIDIdentifier] = coords.ID279infomap[common.DebugWorkspaceIdentifier] = strconv.FormatBool(coords.Debug)280ctx := context.WithValue(r.Context(), common.WorkspaceInfoIdentifier, infomap)281conn, err := gitpod.NewWebsocketConnection(ctx, wsConn, func(staleErr error) {282log.WithError(staleErr).Error("tunnel ssh: closing stale connection")283})284if err != nil {285log.WithError(err).Error("tunnel ssh: upgrade to the WebSocket protocol failed")286return287}288log.Debugf("tunnel ssh: Connected from %s", conn.RemoteAddr())289sshGatewayServer.HandleConn(conn)290log.WithField("duration", time.Since(startTime).Seconds()).Debugf("tunnel ssh: Disconnect from %s", conn.RemoteAddr())291})292}293294func (ir *ideRoutes) HandleDirectSupervisorRoute(route *mux.Route, authenticated bool, proxyPassOpts ...proxyPassOpt) {295r := route.Subrouter()296r.Use(logRouteHandlerHandler(fmt.Sprintf("HandleDirectSupervisorRoute (authenticated: %v)", authenticated)))297r.Use(ir.workspaceMustExistHandler)298if authenticated {299r.Use(ir.Config.WorkspaceAuthHandler)300}301302r.NewRoute().HandlerFunc(proxyPass(ir.Config, ir.InfoProvider, workspacePodSupervisorResolver, proxyPassOpts...)).Name("supervisor")303}304305func (ir *ideRoutes) HandleSupervisorFrontendRoute(route *mux.Route) {306if ir.Config.Config.BlobServer == nil {307// if we don't have blobserve, we serve the supervisor frontend from supervisor directly308ir.HandleDirectSupervisorRoute(route, false)309return310}311312r := route.Subrouter()313r.Use(logRouteHandlerHandler("SupervisorIDEHostHandler"))314r.Use(ir.workspaceMustExistHandler)315// strip the frontend prefix, just for good measure316r.Use(func(h http.Handler) http.Handler {317return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {318req.URL.Path = strings.TrimPrefix(req.URL.Path, "/_supervisor/frontend")319h.ServeHTTP(resp, req)320})321})322// always hit the blobserver to ensure that blob is downloaded323r.NewRoute().HandlerFunc(proxyPass(ir.Config, ir.InfoProvider, func(cfg *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (*url.URL, string, error) {324info := getWorkspaceInfoFromContext(req.Context())325return resolveSupervisorURL(cfg, info, req)326}, func(h *proxyPassConfig) {327h.Transport = &blobserveTransport{328transport: h.Transport,329Config: ir.Config.Config,330resolveImage: func(t *blobserveTransport, req *http.Request) string {331info := getWorkspaceInfoFromContext(req.Context())332if info == nil && len(ir.Config.Config.WorkspacePodConfig.SupervisorImage) == 0 {333// no workspace information available - cannot resolve supervisor image334return ""335}336337// use the config value for backwards compatibility when info.SupervisorImage is not set338image := ir.Config.Config.WorkspacePodConfig.SupervisorImage339if info != nil && len(info.SupervisorImage) > 0 {340image = info.SupervisorImage341}342343path := strings.TrimPrefix(req.URL.Path, "/"+image)344if path == "/worker-proxy.js" {345// worker must be served from the same origin346return ""347}348return image349},350}351}, withUseTargetHost())).Name("supervisor_frontend")352}353354func resolveSupervisorURL(cfg *Config, info *common.WorkspaceInfo, req *http.Request) (*url.URL, string, error) {355if info == nil && len(cfg.WorkspacePodConfig.SupervisorImage) == 0 {356log.WithFields(log.OWI("", getWorkspaceCoords(req).ID, "")).Warn("no workspace info available - cannot resolve supervisor route")357return nil, "", xerrors.Errorf("no workspace information available - cannot resolve supervisor route")358}359360// use the config value for backwards compatibility when info.SupervisorImage is not set361supervisorImage := cfg.WorkspacePodConfig.SupervisorImage362if info != nil && len(info.SupervisorImage) > 0 {363supervisorImage = info.SupervisorImage364}365366var dst url.URL367dst.Scheme = cfg.BlobServer.Scheme368dst.Host = cfg.BlobServer.Host369dst.Path = cfg.BlobServer.PathPrefix + "/" + supervisorImage370return &dst, "blobserve/supervisor", nil371}372373type BlobserveInlineVars struct {374IDE string `json:"ide"`375SupervisorImage string `json:"supervisor"`376}377378func (ir *ideRoutes) HandleRoot(route *mux.Route) {379r := route.Subrouter()380r.Use(logRouteHandlerHandler("handleRoot"))381r.Use(ir.workspaceMustExistHandler)382383proxyPassWoSensitiveCookies := sensitiveCookieHandler(ir.Config.Config.GitpodInstallation.HostName)(proxyPass(ir.Config, ir.InfoProvider, workspacePodResolver))384directIDEPass := ir.Config.WorkspaceAuthHandler(proxyPassWoSensitiveCookies)385386// always hit the blobserver to ensure that blob is downloaded387r.NewRoute().HandlerFunc(proxyPass(ir.Config, ir.InfoProvider, dynamicIDEResolver, func(h *proxyPassConfig) {388h.Transport = &blobserveTransport{389transport: h.Transport,390Config: ir.Config.Config,391resolveImage: func(t *blobserveTransport, req *http.Request) string {392info := getWorkspaceInfoFromContext(req.Context())393if info == nil {394// no workspace information available - cannot resolve IDE image and path395return ""396}397image := info.IDEImage398imagePath := strings.TrimPrefix(req.URL.Path, t.Config.BlobServer.PathPrefix+"/"+image)399if imagePath != "/index.html" && imagePath != "/" {400return image401}402// blobserve can inline static links in index.html for IDE and supervisor to avoid redirects for each supervisor resource403// but it has to know exposed URLs in the context of current workspace cluster404// so first we ask blobserve to preload the supervisor image405// and if it is successful we pass exposed URLs to IDE and supervisor to blobserve for inlining406supervisorURL, supervisorResource, err := resolveSupervisorURL(t.Config, info, req)407if err != nil {408log.WithError(err).Error("could not preload supervisor")409return image410}411supervisorURLString := supervisorURL.String() + "/main.js"412preloadSupervisorReq, err := http.NewRequest("HEAD", supervisorURLString, nil)413if err != nil {414log.WithField("supervisorURL", supervisorURL).WithError(err).Error("could not preload supervisor")415return image416}417preloadSupervisorReq = withResourceMetricsLabel(preloadSupervisorReq, supervisorResource)418preloadSupervisorReq = withHttpVersionMetricsLabel(preloadSupervisorReq)419resp, err := t.DoRoundTrip(preloadSupervisorReq)420if err != nil {421log.WithField("supervisorURL", supervisorURL).WithError(err).Error("could not preload supervisor")422return image423}424_ = resp.Body.Close()425if resp.StatusCode != http.StatusOK {426log.WithField("supervisorURL", supervisorURL).WithField("statusCode", resp.StatusCode).WithField("status", resp.Status).Error("could not preload supervisor")427return image428}429430// use the config value for backwards compatibility when info.SupervisorImage is not set431supervisorImage := t.Config.WorkspacePodConfig.SupervisorImage432if len(info.SupervisorImage) > 0 {433supervisorImage = info.SupervisorImage434}435436inlineVars := &BlobserveInlineVars{437IDE: t.asBlobserveURL(image, ""),438SupervisorImage: t.asBlobserveURL(supervisorImage, ""),439}440inlinveVarsValue, err := json.Marshal(inlineVars)441if err != nil {442log.WithError(err).WithField("inlineVars", inlineVars).Error("could no serialize inline vars")443return image444}445446req.Header.Add("X-BlobServe-InlineVars", string(inlinveVarsValue))447return image448},449}450}, withHTTPErrorHandler(directIDEPass), withUseTargetHost())).Name("root")451}452453func installForeignRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) error {454r.Use(instrumentServerMetrics)455456err := installWorkspacePortRoutes(r.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {457workspacePathPrefix := rm.Vars[common.WorkspacePathPrefixIdentifier]458if workspacePathPrefix == "" || rm.Vars[common.WorkspacePortIdentifier] == "" {459return false460}461r.URL.Path = strings.TrimPrefix(r.URL.Path, workspacePathPrefix)462return true463}).Subrouter(), config, infoProvider)464if err != nil {465return err466}467err = installDebugWorkspaceRoutes(r.MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {468workspacePathPrefix := rm.Vars[common.WorkspacePathPrefixIdentifier]469if workspacePathPrefix == "" || rm.Vars[common.DebugWorkspaceIdentifier] != "true" {470return false471}472r.URL.Path = strings.TrimPrefix(r.URL.Path, workspacePathPrefix)473return true474}).Subrouter(), config, infoProvider)475if err != nil {476return err477}478installForeignBlobserveRoutes(r.NewRoute().Subrouter(), config, infoProvider)479return nil480}481482const imagePathSeparator = "/__files__"483484// installForeignBlobserveRoutes implements long-lived caching with versioned URLs, see https://web.dev/http-cache/#versioned-urls485func installForeignBlobserveRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) {486r.Use(logHandler)487r.Use(logRouteHandlerHandler("BlobserveRootHandler"))488489// filter all session cookies490r.Use(sensitiveCookieHandler(config.Config.GitpodInstallation.HostName))491492targetResolver := func(cfg *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (tgt *url.URL, str string, err error) {493segments := strings.SplitN(req.URL.Path, imagePathSeparator, 2)494if len(segments) < 2 {495return nil, "", xerrors.Errorf("invalid URL")496}497image, path := segments[0], segments[1]498499req.URL.Path = path500501var dst url.URL502dst.Scheme = cfg.BlobServer.Scheme503dst.Host = cfg.BlobServer.Host504dst.Path = cfg.BlobServer.PathPrefix + "/" + strings.TrimPrefix(image, "/")505return &dst, "blobserve/foreign_content", nil506}507r.NewRoute().Handler(proxyPass(config, infoProvider, targetResolver, withLongTermCaching(), withUseTargetHost())).Name("blobserve")508}509510// installDebugWorkspaceRoutes configures for debug workspace.511func installDebugWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) error {512showPortNotFoundPage, err := servePortNotFoundPage(config.Config)513if err != nil {514return err515}516517r.Use(logHandler)518r.Use(config.WorkspaceAuthHandler)519// filter all session cookies520r.Use(sensitiveCookieHandler(config.Config.GitpodInstallation.HostName))521522r.NewRoute().HandlerFunc(proxyPass(config, infoProvider, workspacePodResolver, withHTTPErrorHandler(showPortNotFoundPage)))523return nil524}525526// installWorkspacePortRoutes configures routing for exposed ports.527func installWorkspacePortRoutes(r *mux.Router, config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider) error {528showPortNotFoundPage, err := servePortNotFoundPage(config.Config)529if err != nil {530return err531}532533portTransport := createDefaultTransport(config.Config.TransportConfig, withSkipTLSVerify())534535r.Use(logHandler)536r.Use(config.WorkspaceAuthHandler)537// filter all session cookies538r.Use(sensitiveCookieHandler(config.Config.GitpodInstallation.HostName))539540// forward request to workspace port541r.NewRoute().HandlerFunc(542func(rw http.ResponseWriter, r *http.Request) {543// a work-around for servers which does not respect case-insensitive headers, see https://github.com/gitpod-io/gitpod/issues/4047#issuecomment-856566526544for _, name := range []string{"Key", "Extensions", "Accept", "Protocol", "Version"} {545values := r.Header["Sec-Websocket-"+name]546if len(values) != 0 {547r.Header.Del("Sec-Websocket-" + name)548r.Header["Sec-WebSocket-"+name] = values549}550}551r.Header.Add("X-Forwarded-Proto", "https")552r.Header.Add("X-Forwarded-Host", r.Host)553r.Header.Add("X-Forwarded-Port", "443")554555coords := getWorkspaceCoords(r)556if coords.Debug {557r.Header.Add("X-WS-Proxy-Debug-Port", coords.Port)558}559560proxyPass(561config,562infoProvider,563workspacePodPortResolver,564withHTTPErrorHandler(showPortNotFoundPage),565withXFrameOptionsFilter(),566func(h *proxyPassConfig) {567h.Transport = portTransport568},569)(rw, r)570},571)572573return nil574}575576// workspacePodResolver resolves to the workspace pod's url from the given request.577func workspacePodResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (url *url.URL, resource string, err error) {578coords := getWorkspaceCoords(req)579var port string580if coords.Debug {581resource = "debug_workspace"582port = fmt.Sprint(config.WorkspacePodConfig.IDEDebugPort)583} else {584resource = "workspace"585port = fmt.Sprint(config.WorkspacePodConfig.TheiaPort)586}587workspaceInfo := infoProvider.WorkspaceInfo(coords.ID)588url, err = buildWorkspacePodURL(api.PortProtocol_PORT_PROTOCOL_HTTP, workspaceInfo.IPAddress, port)589return590}591592// workspacePodPortResolver resolves to the workspace pods ports.593func workspacePodPortResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (url *url.URL, resource string, err error) {594coords := getWorkspaceCoords(req)595workspaceInfo := infoProvider.WorkspaceInfo(coords.ID)596var port string597protocol := api.PortProtocol_PORT_PROTOCOL_HTTP598if coords.Debug {599resource = "debug_workspace_port"600port = fmt.Sprint(config.WorkspacePodConfig.DebugWorkspaceProxyPort)601} else {602resource = "workspace_port"603port = coords.Port604prt, err := strconv.ParseUint(port, 10, 16)605if err != nil {606log.WithField("port", port).WithError(err).Error("cannot convert port to int")607} else {608for _, p := range workspaceInfo.Ports {609if p.Port == uint32(prt) {610protocol = p.Protocol611break612}613}614}615}616url, err = buildWorkspacePodURL(protocol, workspaceInfo.IPAddress, port)617return618}619620// workspacePodSupervisorResolver resolves to the workspace pods Supervisor url from the given request.621func workspacePodSupervisorResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (url *url.URL, resource string, err error) {622coords := getWorkspaceCoords(req)623var port string624if coords.Debug {625resource = "debug_workspace/supervisor"626port = fmt.Sprint(config.WorkspacePodConfig.SupervisorDebugPort)627} else {628resource = "workspace/supervisor"629port = fmt.Sprint(config.WorkspacePodConfig.SupervisorPort)630}631workspaceInfo := infoProvider.WorkspaceInfo(coords.ID)632url, err = buildWorkspacePodURL(api.PortProtocol_PORT_PROTOCOL_HTTP, workspaceInfo.IPAddress, port)633return634}635636func dynamicIDEResolver(config *Config, infoProvider common.WorkspaceInfoProvider, req *http.Request) (res *url.URL, resource string, err error) {637info := getWorkspaceInfoFromContext(req.Context())638if info == nil {639log.WithFields(log.OWI("", getWorkspaceCoords(req).ID, "")).Warn("no workspace info available - cannot resolve Theia route")640return nil, "", xerrors.Errorf("no workspace information available - cannot resolve Theia route")641}642643var dst url.URL644dst.Scheme = config.BlobServer.Scheme645dst.Host = config.BlobServer.Host646dst.Path = config.BlobServer.PathPrefix + "/" + info.IDEImage647648return &dst, "blobserve/ide", nil649}650651func buildWorkspacePodURL(protocol api.PortProtocol, ipAddress string, port string) (*url.URL, error) {652portProtocol := ""653switch protocol {654case api.PortProtocol_PORT_PROTOCOL_HTTP:655portProtocol = "http"656case api.PortProtocol_PORT_PROTOCOL_HTTPS:657portProtocol = "https"658default:659return nil, xerrors.Errorf("protocol not supported")660}661return url.Parse(fmt.Sprintf("%v://%v:%v", portProtocol, ipAddress, port))662}663664type wsproxyContextKey struct{}665666var (667logContextValueKey = wsproxyContextKey{}668infoContextValueKey = wsproxyContextKey{}669)670671func logHandler(h http.Handler) http.Handler {672return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {673var (674vars = mux.Vars(req)675wsID = vars[common.WorkspaceIDIdentifier]676port = vars[common.WorkspacePortIdentifier]677)678entry := logrus.Fields{679"workspaceId": wsID,680"portID": port,681"url": req.URL.String(),682}683ctx := context.WithValue(req.Context(), logContextValueKey, entry)684req = req.WithContext(ctx)685686h.ServeHTTP(resp, req)687})688}689690func logRouteHandlerHandler(routeHandlerName string) mux.MiddlewareFunc {691return func(h http.Handler) http.Handler {692return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {693getLog(req.Context()).WithField("routeHandler", routeHandlerName).Debug("hit route handler")694h.ServeHTTP(resp, req)695})696}697}698699func getLog(ctx context.Context) *logrus.Entry {700r := ctx.Value(logContextValueKey)701rl, ok := r.(logrus.Fields)702if rl == nil || !ok {703return log.Log704}705706return log.WithFields(rl)707}708709func sensitiveCookieHandler(domain string) func(h http.Handler) http.Handler {710return func(h http.Handler) http.Handler {711return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {712cookies := removeSensitiveCookies(readCookies(req.Header, ""), domain)713header := make([]string, 0, len(cookies))714for _, c := range cookies {715if c == nil {716continue717}718719cookie := c.String()720if cookie == "" {721// because we're checking for nil above, it must be that the cookie name is invalid.722// Some languages have no quarels with producing invalid cookie names, so we must too.723// See https://github.com/gitpod-io/gitpod/issues/2470 for more details.724var (725originalName = c.Name726replacementName = fmt.Sprintf("name%d%d", rand.Uint64(), time.Now().Unix())727)728c.Name = replacementName729cookie = c.String()730if cookie == "" {731// despite our best efforts, we still couldn't render the cookie. We'll just drop732// it at this point733continue734}735736cookie = strings.Replace(cookie, replacementName, originalName, 1)737c.Name = originalName738}739740header = append(header, cookie)741}742743// using the header string slice here directly would result in multiple cookie header744// being sent. See https://github.com/gitpod-io/gitpod/issues/2121.745req.Header["Cookie"] = []string{strings.Join(header, ";")}746747h.ServeHTTP(resp, req)748})749}750}751752// workspaceMustExistHandler redirects if we don't know about a workspace yet.753func workspaceMustExistHandler(config *Config, infoProvider common.WorkspaceInfoProvider) mux.MiddlewareFunc {754return func(h http.Handler) http.Handler {755return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {756coords := getWorkspaceCoords(req)757info := infoProvider.WorkspaceInfo(coords.ID)758if info == nil {759redirectURL := fmt.Sprintf("%s://%s/start/?not_found=true#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, coords.ID)760http.Redirect(resp, req, redirectURL, http.StatusFound)761return762}763764h.ServeHTTP(resp, req.WithContext(context.WithValue(req.Context(), infoContextValueKey, info)))765})766}767}768769// getWorkspaceInfoFromContext retrieves workspace information put there by the workspaceMustExistHandler.770func getWorkspaceInfoFromContext(ctx context.Context) *common.WorkspaceInfo {771r := ctx.Value(infoContextValueKey)772rl, ok := r.(*common.WorkspaceInfo)773if !ok {774return nil775}776return rl777}778779// removeSensitiveCookies all sensitive cookies from the list.780// This function modifies the slice in-place.781func removeSensitiveCookies(cookies []*http.Cookie, domain string) []*http.Cookie {782hostnamePrefix := domain783for _, c := range []string{" ", "-", "."} {784hostnamePrefix = strings.ReplaceAll(hostnamePrefix, c, "_")785}786hostnamePrefix = "_" + hostnamePrefix + "_"787788n := 0789for _, c := range cookies {790if strings.HasPrefix(c.Name, hostnamePrefix) || strings.HasPrefix(c.Name, "__Host-"+hostnamePrefix) {791// skip session cookies792continue793}794log.WithField("hostnamePrefix", hostnamePrefix).WithField("name", c.Name).Debug("keeping cookie")795cookies[n] = c796n++797}798return cookies[:n]799}800801// region blobserve transport.802type blobserveTransport struct {803transport http.RoundTripper804Config *Config805resolveImage func(t *blobserveTransport, req *http.Request) string806}807808func (t *blobserveTransport) DoRoundTrip(req *http.Request) (resp *http.Response, err error) {809for i := 0; i < 5; i++ {810resp, err = t.transport.RoundTrip(req)811if err != nil {812return nil, err813}814815if resp.StatusCode >= http.StatusBadRequest {816respBody, err := io.ReadAll(resp.Body)817if err != nil {818return nil, err819}820_ = resp.Body.Close()821822if resp.StatusCode == http.StatusServiceUnavailable && string(respBody) == "timeout" {823// on timeout try again till the client request is cancelled824// blob server sometimes takes time to pull a new image825continue826}827828// treat any client or server error code as a http error829return nil, xerrors.Errorf("blobserver error: (%d) %s", resp.StatusCode, string(respBody))830}831break832}833return resp, err834}835836func isWebSocketUpgrade(req *http.Request) bool {837return strings.EqualFold(req.Header.Get("Upgrade"), "websocket") &&838strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade")839}840841func (t *blobserveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {842if isWebSocketUpgrade(req) {843return nil, xerrors.Errorf("blobserve: websocket not supported")844}845846image := t.resolveImage(t, req)847848resp, err = t.DoRoundTrip(req)849if err != nil {850return nil, err851}852853if resp.StatusCode != http.StatusOK {854// only redirect successful responses855return resp, nil856}857858if req.URL.RawQuery != "" {859// URLs with query cannot be static, i.e. the server is required to resolve the query860return resp, nil861}862863// region use fetch metadata to avoid redirections https://developer.mozilla.org/en-US/docs/Glossary/Fetch_metadata_request_header864mode := req.Header.Get("Sec-Fetch-Mode")865dest := req.Header.Get("Sec-Fetch-Dest")866if mode == "" && strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html") {867// fallback for user agents not supporting fetch metadata to avoid redirecting on user navigation868mode = "navigate"869}870if mode == "navigate" || mode == "nested-navigate" || mode == "websocket" {871// user navigation and websocket requests should not be redirected872return resp, nil873}874875if mode == "same-origin" && !(dest == "worker" || dest == "sharedworker") {876// same origin should not be redirected, except workers877// supervisor installs the worker proxy from the workspace origin serving content from the blobserve origin878return resp, nil879}880// endregion881882if image == "" {883return resp, nil884}885886_ = resp.Body.Close()887return t.redirect(image, req)888}889890func (t *blobserveTransport) redirect(image string, req *http.Request) (*http.Response, error) {891path := strings.TrimPrefix(req.URL.Path, t.Config.BlobServer.PathPrefix+"/"+image)892location := t.asBlobserveURL(image, path)893894header := make(http.Header, 2)895header.Set("Location", location)896header.Set("Content-Type", "text/html; charset=utf-8")897898code := http.StatusSeeOther899var (900status = http.StatusText(code)901content = []byte("<a href=\"" + location + "\">" + status + "</a>.\n\n")902)903904return &http.Response{905Request: req,906Header: header,907Body: io.NopCloser(bytes.NewReader(content)),908ContentLength: int64(len(content)),909StatusCode: code,910Status: status,911}, nil912}913914func (t *blobserveTransport) asBlobserveURL(image string, path string) string {915return fmt.Sprintf("%s://ide.%s/blobserve/%s%s%s",916t.Config.GitpodInstallation.Scheme,917t.Config.GitpodInstallation.HostName,918image,919imagePathSeparator,920path,921)922}923924// endregion925926const (927builtinPagePortNotFound = "port-not-found.html"928)929930func servePortNotFoundPage(config *Config) (http.Handler, error) {931fn := filepath.Join(config.BuiltinPages.Location, builtinPagePortNotFound)932if tp := os.Getenv("TELEPRESENCE_ROOT"); tp != "" {933fn = filepath.Join(tp, fn)934}935page, err := os.ReadFile(fn)936if err != nil {937return nil, err938}939page = bytes.ReplaceAll(page, []byte("https://gitpod.io"), []byte(fmt.Sprintf("%s://%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName)))940941return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {942w.WriteHeader(http.StatusNotFound)943_, _ = w.Write(page)944}), nil945}946947948