Path: blob/main/components/ws-proxy/pkg/proxy/workspacerouter.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"fmt"8"net/http"9"path/filepath"10"regexp"11"strings"1213"github.com/gorilla/mux"1415"github.com/gitpod-io/gitpod/common-go/log"16"github.com/gitpod-io/gitpod/common-go/namegen"17"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"18)1920const (21// The header that is used to communicate the "Host" from proxy -> ws-proxy in scenarios where ws-proxy is _not_ directly exposed.22forwardedHostnameHeader = "x-wsproxy-host"2324// This pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21).25workspacePortRegex = "(?P<" + common.WorkspacePortIdentifier + ">[0-9]+)-"2627debugWorkspaceRegex = "(?P<" + common.DebugWorkspaceIdentifier + ">debug-)?"28)2930// This pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21).31// "(?P<" + workspaceIDIdentifier + ">[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11})"32var workspaceIDRegex = fmt.Sprintf("(?P<%s>%s)", common.WorkspaceIDIdentifier, strings.Join(namegen.PossibleWorkspaceIDPatterns, "|"))3334// WorkspaceRouter is a function that configures subrouters (one for theia, one for the exposed ports) on the given router35// which resolve workspace coordinates (ID, port?) from each request. The contract is to store those in the request's mux.Vars36// with the keys workspacePortIdentifier and workspaceIDIdentifier.37type WorkspaceRouter func(r *mux.Router, wsInfoProvider common.WorkspaceInfoProvider) (ideRouter *mux.Router, portRouter *mux.Router, blobserveRouter *mux.Router)3839// HostBasedRouter is a WorkspaceRouter that routes simply based on the "Host" header.40func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) WorkspaceRouter {41return func(r *mux.Router, wsInfoProvider common.WorkspaceInfoProvider) (*mux.Router, *mux.Router, *mux.Router) {42allClusterWsHostSuffixRegex := wsHostSuffixRegex43if allClusterWsHostSuffixRegex == "" {44allClusterWsHostSuffixRegex = wsHostSuffix45}4647// make sure acme router is the first handler setup to make sure it has a chance to catch acme challenge48setupAcmeRouter(r)4950var (51getHostHeader = func(req *http.Request) string {52host := req.Header.Get(header)53// if we don't get host from special header, fallback to use req.Host54if header == "Host" || host == "" {55parts := strings.Split(req.Host, ":")56return parts[0]57}58return host59}60foreignRouter = r.MatcherFunc(matchForeignHostHeader(wsHostSuffix, getHostHeader)).Subrouter()61portRouter = r.MatcherFunc(matchWorkspaceHostHeader(wsHostSuffix, getHostHeader, true)).Subrouter()62ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader, false)).Subrouter()63)6465r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {66hostname := getHostHeader(req)67log.Debugf("no match for path %s, host: %s", req.URL.Path, hostname)68w.WriteHeader(http.StatusNotFound)69})70return ideRouter, portRouter, foreignRouter71}72}7374type hostHeaderProvider func(req *http.Request) string7576func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider, matchPort bool) mux.MatcherFunc {77var regexPrefix string78if matchPort {79regexPrefix = workspacePortRegex + debugWorkspaceRegex + workspaceIDRegex80} else {81regexPrefix = debugWorkspaceRegex + workspaceIDRegex82}8384r := regexp.MustCompile("^" + regexPrefix + wsHostSuffix)8586return func(req *http.Request, m *mux.RouteMatch) bool {87hostname := headerProvider(req)88if hostname == "" {89return false90}9192var workspaceID, workspacePort, debugWorkspace string93matches := r.FindStringSubmatch(hostname)94if len(matches) < 3 {95return false96}97if matchPort {98if len(matches) < 4 {99return false100}101// https://3000-debug-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html102// debugWorkspace: true103// workspaceID: coral-dragon-ilr0r6eq104// workspacePort: 3000105if matches[2] != "" {106debugWorkspace = "true"107}108// https://3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html109// debugWorkspace:110// workspaceID: coral-dragon-ilr0r6eq111// workspacePort: 3000112workspaceID = matches[3]113workspacePort = matches[1]114} else {115if len(matches) < 3 {116return false117}118// https://debug-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html119// debugWorkspace: true120// workspaceID: coral-dragon-ilr0r6eq121// workspacePort:122if matches[1] != "" {123debugWorkspace = "true"124}125126// https://coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html127// debugWorkspace:128// workspaceID: coral-dragon-ilr0r6eq129// workspacePort:130workspaceID = matches[2]131}132133if workspaceID == "" {134return false135}136137if matchPort && workspacePort == "" {138return false139}140141if m.Vars == nil {142m.Vars = make(map[string]string)143}144m.Vars[common.WorkspaceIDIdentifier] = workspaceID145if workspacePort != "" {146m.Vars[common.WorkspacePortIdentifier] = workspacePort147}148if debugWorkspace != "" {149m.Vars[common.DebugWorkspaceIdentifier] = debugWorkspace150}151152return true153}154}155156func matchForeignHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {157pathPortRegex := regexp.MustCompile("^/" + workspacePortRegex + debugWorkspaceRegex + workspaceIDRegex + "/")158pathDebugRegex := regexp.MustCompile("^/" + debugWorkspaceRegex + workspaceIDRegex + "/")159160r := regexp.MustCompile("^(?:v--)?[0-9a-v]+" + wsHostSuffix)161return func(req *http.Request, m *mux.RouteMatch) (result bool) {162hostname := headerProvider(req)163if hostname == "" {164return165}166167matches := r.FindStringSubmatch(hostname)168if len(matches) < 1 {169return170}171172result = true173174if m.Vars == nil {175m.Vars = make(map[string]string)176}177178m.Vars[common.ForeignContentIdentifier] = "true"179180var pathPrefix, workspaceID, workspacePort, debugWorkspace string181matches = pathPortRegex.FindStringSubmatch(req.URL.Path)182if len(matches) < 4 {183matches = pathDebugRegex.FindStringSubmatch(req.URL.Path)184if len(matches) < 3 {185return186}187// 0 => pathPrefix188pathPrefix = matches[0]189// 1 => debug190if matches[1] != "" {191debugWorkspace = "true"192}193// 2 => workspaceId194workspaceID = matches[2]195} else {196// 0 => pathPrefix197pathPrefix = matches[0]198// 1 => port199workspacePort = matches[1]200// 2 => debug201if matches[2] != "" {202debugWorkspace = "true"203}204// 3 => workspaceId205workspaceID = matches[3]206}207208if pathPrefix == "" {209return210}211212if m.Vars == nil {213m.Vars = make(map[string]string)214}215216m.Vars[common.WorkspacePathPrefixIdentifier] = strings.TrimRight(pathPrefix, "/")217m.Vars[common.WorkspaceIDIdentifier] = workspaceID218m.Vars[common.DebugWorkspaceIdentifier] = debugWorkspace219m.Vars[common.WorkspacePortIdentifier] = workspacePort220221return222}223}224225func getWorkspaceCoords(req *http.Request) common.WorkspaceCoords {226vars := mux.Vars(req)227return common.WorkspaceCoords{228ID: vars[common.WorkspaceIDIdentifier],229Port: vars[common.WorkspacePortIdentifier],230Debug: vars[common.DebugWorkspaceIdentifier] == "true",231Foreign: vars[common.ForeignContentIdentifier] == "true",232}233}234235func isAcmeChallenge(path string) bool {236return strings.HasPrefix(filepath.Clean(path), "/.well-known/acme-challenge/")237}238239func matchAcmeChallenge() mux.MatcherFunc {240return func(req *http.Request, m *mux.RouteMatch) bool {241return isAcmeChallenge(req.URL.Path)242}243}244245func setupAcmeRouter(router *mux.Router) {246router.MatcherFunc(matchAcmeChallenge()).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {247log.Debugf("ACME challenge found for path %s, host: %s", req.URL.Path, req.Host)248w.WriteHeader(http.StatusForbidden)249w.Header().Set("Content-Type", "text/plain; charset=utf-8")250})251}252253254