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_test.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
"net/http"
9
"net/http/httptest"
10
"net/url"
11
"testing"
12
13
"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"
14
"github.com/google/go-cmp/cmp"
15
"github.com/gorilla/mux"
16
)
17
18
func TestWorkspaceRouter(t *testing.T) {
19
const wsHostRegex = "\\.ws\\.gitpod\\.dev"
20
const wsHostSuffix = ".ws.gitpod.dev"
21
type Expectation struct {
22
WorkspaceID string
23
WorkspacePort string
24
Status int
25
URL string
26
AdditionalHitCount int
27
DebugWorkspace string
28
}
29
tests := []struct {
30
Name string
31
URL string
32
Headers map[string]string
33
WSHostSuffix string
34
Router WorkspaceRouter
35
Infos []common.WorkspaceInfo
36
Expected Expectation
37
}{
38
{
39
Name: "host-based workspace access",
40
URL: "http://amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
41
Headers: map[string]string{
42
forwardedHostnameHeader: "amaranth-smelt-9ba20cc1.ws.gitpod.dev",
43
},
44
Router: HostBasedRouter(forwardedHostnameHeader, wsHostSuffix, wsHostRegex),
45
WSHostSuffix: wsHostSuffix,
46
Expected: Expectation{
47
WorkspaceID: "amaranth-smelt-9ba20cc1",
48
Status: http.StatusOK,
49
URL: "http://amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
50
},
51
},
52
{
53
Name: "host-based debug workspace access",
54
URL: "http://debug-amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
55
Headers: map[string]string{
56
forwardedHostnameHeader: "debug-amaranth-smelt-9ba20cc1.ws.gitpod.dev",
57
},
58
Router: HostBasedRouter(forwardedHostnameHeader, wsHostSuffix, wsHostRegex),
59
WSHostSuffix: wsHostSuffix,
60
Expected: Expectation{
61
DebugWorkspace: "true",
62
WorkspaceID: "amaranth-smelt-9ba20cc1",
63
Status: http.StatusOK,
64
URL: "http://debug-amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
65
},
66
},
67
{
68
Name: "host-based port access",
69
URL: "http://1234-amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
70
Headers: map[string]string{
71
forwardedHostnameHeader: "1234-amaranth-smelt-9ba20cc1.ws.gitpod.dev",
72
},
73
Router: HostBasedRouter(forwardedHostnameHeader, wsHostSuffix, wsHostRegex),
74
WSHostSuffix: wsHostSuffix,
75
Expected: Expectation{
76
WorkspaceID: "amaranth-smelt-9ba20cc1",
77
WorkspacePort: "1234",
78
Status: http.StatusOK,
79
URL: "http://1234-amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
80
},
81
},
82
{
83
Name: "host-based debug port access",
84
URL: "http://1234-debug-amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
85
Headers: map[string]string{
86
forwardedHostnameHeader: "1234-debug-amaranth-smelt-9ba20cc1.ws.gitpod.dev",
87
},
88
Router: HostBasedRouter(forwardedHostnameHeader, wsHostSuffix, wsHostRegex),
89
WSHostSuffix: wsHostSuffix,
90
Expected: Expectation{
91
DebugWorkspace: "true",
92
WorkspaceID: "amaranth-smelt-9ba20cc1",
93
WorkspacePort: "1234",
94
Status: http.StatusOK,
95
URL: "http://1234-debug-amaranth-smelt-9ba20cc1.ws.gitpod.dev/",
96
},
97
},
98
}
99
100
for _, test := range tests {
101
t.Run(test.Name, func(t *testing.T) {
102
r := mux.NewRouter()
103
ideRouter, portRouter, blobserveRouter := test.Router(r, &fakeWsInfoProvider{infos: test.Infos})
104
var act Expectation
105
actRecorder := func(w http.ResponseWriter, req *http.Request) {
106
defer w.WriteHeader(http.StatusOK)
107
108
vars := mux.Vars(req)
109
if vars == nil {
110
return
111
}
112
113
act.WorkspaceID = vars[common.WorkspaceIDIdentifier]
114
act.WorkspacePort = vars[common.WorkspacePortIdentifier]
115
act.DebugWorkspace = vars[common.DebugWorkspaceIdentifier]
116
act.URL = req.URL.String()
117
act.AdditionalHitCount++
118
}
119
120
if ideRouter != nil {
121
ideRouter.HandleFunc("/", actRecorder)
122
ideRouter.HandleFunc("/services", actRecorder)
123
}
124
if portRouter != nil {
125
portRouter.HandleFunc("/", actRecorder)
126
}
127
if blobserveRouter != nil {
128
blobserveRouter.HandleFunc("/", actRecorder)
129
blobserveRouter.HandleFunc("/image:version:/foo/main.js", actRecorder)
130
}
131
132
// build artificial request
133
req, err := http.NewRequest("GET", test.URL, nil)
134
if err != nil {
135
t.Fatal(err)
136
}
137
for key, value := range test.Headers {
138
req.Header.Add(key, value)
139
}
140
141
// "send" artificial request with response mock
142
rr := httptest.NewRecorder()
143
r.ServeHTTP(rr, req)
144
145
// we don't count the first hit to make the expectation structure easier.
146
act.AdditionalHitCount--
147
148
act.Status = rr.Code
149
150
if diff := cmp.Diff(test.Expected, act); diff != "" {
151
t.Errorf("unexpected response (-want +got):\n%s", diff)
152
}
153
})
154
}
155
}
156
157
func TestMatchWorkspaceHostHeader(t *testing.T) {
158
type matchResult struct {
159
MatchesWorkspace bool
160
MatchesPort bool
161
WorkspaceVars map[string]string
162
PortVars map[string]string
163
}
164
165
wsHostSuffix := ".gitpod.io"
166
tests := []struct {
167
Name string
168
HostHeader string
169
Path string
170
Expected matchResult
171
}{
172
{
173
Name: "no match",
174
HostHeader: "foobar.com",
175
},
176
{
177
Name: "no host",
178
HostHeader: "",
179
},
180
{
181
Name: "no match 2",
182
HostHeader: "0d9rkrj560blqb5s07q431ru9mhg19k1k4bqgd1dbprtgmt7vuhk" + wsHostSuffix,
183
Path: "eu.gcr.io/gitpod-core-dev/build/ide/code:nightly@sha256:41aeea688aa0943bd746cb70c4ed378910f7c7ecf56f5f53ccb2b76c6b68e1a7/__files__/index.html",
184
},
185
{
186
Name: "no match 3",
187
HostHeader: "v--0d9rkrj560blqb5s07q431ru9mhg19k1k4bqgd1dbprtgmt7vuhk" + wsHostSuffix,
188
Path: "eu.gcr.io/gitpod-core-dev/build/ide/code:nightly@sha256:41aeea688aa0943bd746cb70c4ed378910f7c7ecf56f5f53ccb2b76c6b68e1a7/__files__/index.html",
189
},
190
{
191
Name: "workspace match",
192
HostHeader: "amaranth-smelt-9ba20cc1" + wsHostSuffix,
193
Expected: matchResult{
194
MatchesWorkspace: true,
195
WorkspaceVars: map[string]string{
196
common.WorkspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
197
},
198
},
199
},
200
{
201
Name: "port match",
202
HostHeader: "8080-amaranth-smelt-9ba20cc1" + wsHostSuffix,
203
Expected: matchResult{
204
MatchesPort: true,
205
PortVars: map[string]string{
206
common.WorkspaceIDIdentifier: "amaranth-smelt-9ba20cc1",
207
common.WorkspacePortIdentifier: "8080",
208
},
209
},
210
},
211
}
212
for _, test := range tests {
213
t.Run(test.Name, func(t *testing.T) {
214
req := &http.Request{
215
Host: test.HostHeader,
216
URL: &url.URL{
217
Path: test.Path,
218
},
219
Method: http.MethodGet,
220
Header: http.Header{
221
forwardedHostnameHeader: []string{test.HostHeader},
222
},
223
}
224
225
prov := func(req *http.Request) string { return test.HostHeader }
226
227
wsMatch := mux.RouteMatch{Vars: make(map[string]string)}
228
matchesWS := matchWorkspaceHostHeader(wsHostSuffix, prov, false)(req, &wsMatch)
229
portMatch := mux.RouteMatch{Vars: make(map[string]string)}
230
matchesPort := matchWorkspaceHostHeader(wsHostSuffix, prov, true)(req, &portMatch)
231
res := matchResult{
232
MatchesPort: matchesPort,
233
MatchesWorkspace: matchesWS,
234
PortVars: portMatch.Vars,
235
WorkspaceVars: wsMatch.Vars,
236
}
237
if len(res.PortVars) == 0 {
238
res.PortVars = nil
239
}
240
if len(res.WorkspaceVars) == 0 {
241
res.WorkspaceVars = nil
242
}
243
244
if diff := cmp.Diff(test.Expected, res); diff != "" {
245
t.Errorf("unexpected response (-want +got):\n%s", diff)
246
}
247
})
248
}
249
}
250
251
func TestAcmeHandler(t *testing.T) {
252
type Expectation struct {
253
ContentType string
254
Code int
255
}
256
tests := []struct {
257
Name string
258
Method string
259
URL string
260
Body []byte
261
Expectation Expectation
262
}{
263
{
264
Name: "Valid acme request",
265
Method: http.MethodGet,
266
URL: "http://domain.example.com/.well-known/acme-challenge/token1",
267
Expectation: Expectation{
268
Code: 403,
269
ContentType: "text/plain; charset=utf-8",
270
},
271
},
272
{
273
Name: "Not an acme request",
274
Method: http.MethodGet,
275
URL: "http://domain.example.com/",
276
Expectation: Expectation{
277
Code: 404,
278
ContentType: "text/plain; charset=utf-8",
279
},
280
},
281
{
282
Name: "Valid acme request",
283
Method: http.MethodGet,
284
URL: "http://1.1.1.1/.well-known/acme-challenge/token1",
285
Expectation: Expectation{
286
Code: 403,
287
ContentType: "text/plain; charset=utf-8",
288
},
289
},
290
}
291
292
for _, test := range tests {
293
t.Run(test.Name, func(t *testing.T) {
294
req, err := http.NewRequest(test.Method, test.URL, nil)
295
if err != nil {
296
t.Errorf("unexpected error:%v", err)
297
}
298
299
w := httptest.NewRecorder()
300
301
r := mux.NewRouter()
302
setupAcmeRouter(r)
303
304
r.ServeHTTP(w, req)
305
306
act := Expectation{
307
ContentType: w.Header().Get("Content-Type"),
308
Code: w.Code,
309
}
310
311
if diff := cmp.Diff(test.Expectation, act); diff != "" {
312
t.Errorf("unexpected result (-want +got):\n%s", diff)
313
}
314
})
315
}
316
}
317
318