Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-proxy/pkg/proxy/workspacerouter.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
"fmt"
9
"net/http"
10
"path/filepath"
11
"regexp"
12
"strings"
13
14
"github.com/gorilla/mux"
15
16
"github.com/gitpod-io/gitpod/common-go/log"
17
"github.com/gitpod-io/gitpod/common-go/namegen"
18
"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"
19
)
20
21
const (
22
// The header that is used to communicate the "Host" from proxy -> ws-proxy in scenarios where ws-proxy is _not_ directly exposed.
23
forwardedHostnameHeader = "x-wsproxy-host"
24
25
// This pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21).
26
workspacePortRegex = "(?P<" + common.WorkspacePortIdentifier + ">[0-9]+)-"
27
28
debugWorkspaceRegex = "(?P<" + common.DebugWorkspaceIdentifier + ">debug-)?"
29
)
30
31
// This pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21).
32
// "(?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})"
33
var workspaceIDRegex = fmt.Sprintf("(?P<%s>%s)", common.WorkspaceIDIdentifier, strings.Join(namegen.PossibleWorkspaceIDPatterns, "|"))
34
35
// WorkspaceRouter is a function that configures subrouters (one for theia, one for the exposed ports) on the given router
36
// which resolve workspace coordinates (ID, port?) from each request. The contract is to store those in the request's mux.Vars
37
// with the keys workspacePortIdentifier and workspaceIDIdentifier.
38
type WorkspaceRouter func(r *mux.Router, wsInfoProvider common.WorkspaceInfoProvider) (ideRouter *mux.Router, portRouter *mux.Router, blobserveRouter *mux.Router)
39
40
// HostBasedRouter is a WorkspaceRouter that routes simply based on the "Host" header.
41
func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) WorkspaceRouter {
42
return func(r *mux.Router, wsInfoProvider common.WorkspaceInfoProvider) (*mux.Router, *mux.Router, *mux.Router) {
43
allClusterWsHostSuffixRegex := wsHostSuffixRegex
44
if allClusterWsHostSuffixRegex == "" {
45
allClusterWsHostSuffixRegex = wsHostSuffix
46
}
47
48
// make sure acme router is the first handler setup to make sure it has a chance to catch acme challenge
49
setupAcmeRouter(r)
50
51
var (
52
getHostHeader = func(req *http.Request) string {
53
host := req.Header.Get(header)
54
// if we don't get host from special header, fallback to use req.Host
55
if header == "Host" || host == "" {
56
parts := strings.Split(req.Host, ":")
57
return parts[0]
58
}
59
return host
60
}
61
foreignRouter = r.MatcherFunc(matchForeignHostHeader(wsHostSuffix, getHostHeader)).Subrouter()
62
portRouter = r.MatcherFunc(matchWorkspaceHostHeader(wsHostSuffix, getHostHeader, true)).Subrouter()
63
ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader, false)).Subrouter()
64
)
65
66
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
67
hostname := getHostHeader(req)
68
log.Debugf("no match for path %s, host: %s", req.URL.Path, hostname)
69
w.WriteHeader(http.StatusNotFound)
70
})
71
return ideRouter, portRouter, foreignRouter
72
}
73
}
74
75
type hostHeaderProvider func(req *http.Request) string
76
77
func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider, matchPort bool) mux.MatcherFunc {
78
var regexPrefix string
79
if matchPort {
80
regexPrefix = workspacePortRegex + debugWorkspaceRegex + workspaceIDRegex
81
} else {
82
regexPrefix = debugWorkspaceRegex + workspaceIDRegex
83
}
84
85
r := regexp.MustCompile("^" + regexPrefix + wsHostSuffix)
86
87
return func(req *http.Request, m *mux.RouteMatch) bool {
88
hostname := headerProvider(req)
89
if hostname == "" {
90
return false
91
}
92
93
var workspaceID, workspacePort, debugWorkspace string
94
matches := r.FindStringSubmatch(hostname)
95
if len(matches) < 3 {
96
return false
97
}
98
if matchPort {
99
if len(matches) < 4 {
100
return false
101
}
102
// https://3000-debug-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
103
// debugWorkspace: true
104
// workspaceID: coral-dragon-ilr0r6eq
105
// workspacePort: 3000
106
if matches[2] != "" {
107
debugWorkspace = "true"
108
}
109
// https://3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
110
// debugWorkspace:
111
// workspaceID: coral-dragon-ilr0r6eq
112
// workspacePort: 3000
113
workspaceID = matches[3]
114
workspacePort = matches[1]
115
} else {
116
if len(matches) < 3 {
117
return false
118
}
119
// https://debug-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
120
// debugWorkspace: true
121
// workspaceID: coral-dragon-ilr0r6eq
122
// workspacePort:
123
if matches[1] != "" {
124
debugWorkspace = "true"
125
}
126
127
// https://coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
128
// debugWorkspace:
129
// workspaceID: coral-dragon-ilr0r6eq
130
// workspacePort:
131
workspaceID = matches[2]
132
}
133
134
if workspaceID == "" {
135
return false
136
}
137
138
if matchPort && workspacePort == "" {
139
return false
140
}
141
142
if m.Vars == nil {
143
m.Vars = make(map[string]string)
144
}
145
m.Vars[common.WorkspaceIDIdentifier] = workspaceID
146
if workspacePort != "" {
147
m.Vars[common.WorkspacePortIdentifier] = workspacePort
148
}
149
if debugWorkspace != "" {
150
m.Vars[common.DebugWorkspaceIdentifier] = debugWorkspace
151
}
152
153
return true
154
}
155
}
156
157
func matchForeignHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
158
pathPortRegex := regexp.MustCompile("^/" + workspacePortRegex + debugWorkspaceRegex + workspaceIDRegex + "/")
159
pathDebugRegex := regexp.MustCompile("^/" + debugWorkspaceRegex + workspaceIDRegex + "/")
160
161
r := regexp.MustCompile("^(?:v--)?[0-9a-v]+" + wsHostSuffix)
162
return func(req *http.Request, m *mux.RouteMatch) (result bool) {
163
hostname := headerProvider(req)
164
if hostname == "" {
165
return
166
}
167
168
matches := r.FindStringSubmatch(hostname)
169
if len(matches) < 1 {
170
return
171
}
172
173
result = true
174
175
if m.Vars == nil {
176
m.Vars = make(map[string]string)
177
}
178
179
m.Vars[common.ForeignContentIdentifier] = "true"
180
181
var pathPrefix, workspaceID, workspacePort, debugWorkspace string
182
matches = pathPortRegex.FindStringSubmatch(req.URL.Path)
183
if len(matches) < 4 {
184
matches = pathDebugRegex.FindStringSubmatch(req.URL.Path)
185
if len(matches) < 3 {
186
return
187
}
188
// 0 => pathPrefix
189
pathPrefix = matches[0]
190
// 1 => debug
191
if matches[1] != "" {
192
debugWorkspace = "true"
193
}
194
// 2 => workspaceId
195
workspaceID = matches[2]
196
} else {
197
// 0 => pathPrefix
198
pathPrefix = matches[0]
199
// 1 => port
200
workspacePort = matches[1]
201
// 2 => debug
202
if matches[2] != "" {
203
debugWorkspace = "true"
204
}
205
// 3 => workspaceId
206
workspaceID = matches[3]
207
}
208
209
if pathPrefix == "" {
210
return
211
}
212
213
if m.Vars == nil {
214
m.Vars = make(map[string]string)
215
}
216
217
m.Vars[common.WorkspacePathPrefixIdentifier] = strings.TrimRight(pathPrefix, "/")
218
m.Vars[common.WorkspaceIDIdentifier] = workspaceID
219
m.Vars[common.DebugWorkspaceIdentifier] = debugWorkspace
220
m.Vars[common.WorkspacePortIdentifier] = workspacePort
221
222
return
223
}
224
}
225
226
func getWorkspaceCoords(req *http.Request) common.WorkspaceCoords {
227
vars := mux.Vars(req)
228
return common.WorkspaceCoords{
229
ID: vars[common.WorkspaceIDIdentifier],
230
Port: vars[common.WorkspacePortIdentifier],
231
Debug: vars[common.DebugWorkspaceIdentifier] == "true",
232
Foreign: vars[common.ForeignContentIdentifier] == "true",
233
}
234
}
235
236
func isAcmeChallenge(path string) bool {
237
return strings.HasPrefix(filepath.Clean(path), "/.well-known/acme-challenge/")
238
}
239
240
func matchAcmeChallenge() mux.MatcherFunc {
241
return func(req *http.Request, m *mux.RouteMatch) bool {
242
return isAcmeChallenge(req.URL.Path)
243
}
244
}
245
246
func setupAcmeRouter(router *mux.Router) {
247
router.MatcherFunc(matchAcmeChallenge()).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
248
log.Debugf("ACME challenge found for path %s, host: %s", req.URL.Path, req.Host)
249
w.WriteHeader(http.StatusForbidden)
250
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
251
})
252
}
253
254