@@ -59,28 +59,34 @@ var nonCanonicalHeaders = map[string]string{
59
59
"Sec-Websocket-Version" :"Sec-WebSocket-Version" ,
60
60
}
61
61
62
- //WorkspaceAppServer serves workspace apps endpoints, including:
62
+ //Server serves workspace apps endpoints, including:
63
63
// - Path-based apps
64
64
// - Subdomain app middleware
65
65
// - Workspace reconnecting-pty (aka. web terminal)
66
- type WorkspaceAppServer struct {
66
+ type Server struct {
67
67
Logger slog.Logger
68
68
69
- // PrimaryAccessURL should be a url to the coderd dashboard. This can be the
70
- // same as the AccessURL if the WorkspaceAppServer is embedded.
71
- PrimaryAccessURL * url.URL
72
- AccessURL * url.URL
73
- AppHostname string
74
- AppHostnameRegex * regexp.Regexp
69
+ // DashboardURL should be a url to the coderd dashboard. This can be the
70
+ // same as the AccessURL if the Server is embedded.
71
+ DashboardURL * url.URL
72
+ AccessURL * url.URL
73
+ // Hostname should be the wildcard hostname to use for workspace
74
+ // applications INCLUDING the asterisk, (optional) suffix and leading dot.
75
+ // It will use the same scheme and port number as the access URL.
76
+ // E.g. "*.apps.coder.com" or "*-apps.coder.com".
77
+ Hostname string
78
+ // HostnameRegex contains the regex version of Hostname as generated by
79
+ // httpapi.CompileHostnamePattern(). It MUST be set if Hostname is set.
80
+ HostnameRegex * regexp.Regexp
75
81
DeploymentValues * codersdk.DeploymentValues
76
82
RealIPConfig * httpmw.RealIPConfig
77
83
78
- TokenProvider SignedTokenProvider
79
- WorkspaceConnCache * wsconncache.Cache
80
- AppSigningKey AppSigningKey
84
+ SignedTokenProvider SignedTokenProvider
85
+ WorkspaceConnCache * wsconncache.Cache
86
+ AppSigningKey SigningKey
81
87
}
82
88
83
- func (s * WorkspaceAppServer )Attach (r chi.Router ,pathAppRateLimiter func (http.Handler ) http.Handler ) {
89
+ func (s * Server )Attach (r chi.Router ,pathAppRateLimiter func (http.Handler ) http.Handler ) {
84
90
servePathApps := func (r chi.Router ) {
85
91
r .Use (pathAppRateLimiter )
86
92
r .HandleFunc ("/*" ,s .workspaceAppsProxyPath )
@@ -97,14 +103,14 @@ func (s *WorkspaceAppServer) Attach(r chi.Router, pathAppRateLimiter func(http.H
97
103
98
104
// workspaceAppsProxyPath proxies requests to a workspace application
99
105
// through a relative URL path.
100
- func (s * WorkspaceAppServer )workspaceAppsProxyPath (rw http.ResponseWriter ,r * http.Request ) {
106
+ func (s * Server )workspaceAppsProxyPath (rw http.ResponseWriter ,r * http.Request ) {
101
107
if s .DeploymentValues .DisablePathApps .Value () {
102
108
site .RenderStaticErrorPage (rw ,r , site.ErrorPageData {
103
109
Status :http .StatusUnauthorized ,
104
110
Title :"Unauthorized" ,
105
111
Description :"Path-based applications are disabled on this Coder deployment by the administrator." ,
106
112
RetryEnabled :false ,
107
- DashboardURL :s .PrimaryAccessURL .String (),
113
+ DashboardURL :s .DashboardURL .String (),
108
114
})
109
115
return
110
116
}
@@ -117,7 +123,7 @@ func (s *WorkspaceAppServer) workspaceAppsProxyPath(rw http.ResponseWriter, r *h
117
123
Title :"Application Not Found" ,
118
124
Description :"Applications must be accessed with the full username, not @me." ,
119
125
RetryEnabled :false ,
120
- DashboardURL :s .PrimaryAccessURL .String (),
126
+ DashboardURL :s .DashboardURL .String (),
121
127
})
122
128
return
123
129
}
@@ -132,7 +138,7 @@ func (s *WorkspaceAppServer) workspaceAppsProxyPath(rw http.ResponseWriter, r *h
132
138
133
139
// ResolveRequest will only return a new signed token if the actor has the RBAC
134
140
// permissions to connect to a workspace.
135
- token ,ok := ResolveRequest (s .Logger ,s .AccessURL ,s .TokenProvider ,rw ,r ,Request {
141
+ token ,ok := ResolveRequest (s .Logger ,s .DashboardURL ,s .SignedTokenProvider ,rw ,r ,Request {
136
142
AccessMethod :AccessMethodPath ,
137
143
BasePath :basePath ,
138
144
UsernameOrID :chi .URLParam (r ,"user" ),
@@ -152,20 +158,20 @@ func (s *WorkspaceAppServer) workspaceAppsProxyPath(rw http.ResponseWriter, r *h
152
158
// DevURLs in Coder V1).
153
159
//
154
160
// There are a lot of paths here:
155
- // 1. If api.AppHostname is not set then we pass on.
161
+ // 1. If api.Hostname is not set then we pass on.
156
162
// 2. If we can't read the request hostname then we return a 400.
157
163
// 3. If the request hostname matches api.AccessURL then we pass on.
158
164
// 5. We split the subdomain into the subdomain and the "rest". If there are no
159
165
// periods in the hostname then we pass on.
160
166
// 5. We parse the subdomain into a httpapi.ApplicationURL struct. If we
161
167
// encounter an error:
162
- // a. If the "rest" does not match api.AppHostname then we pass on;
168
+ // a. If the "rest" does not match api.Hostname then we pass on;
163
169
// b. Otherwise, we return a 400.
164
- // 6. Finally, we verify that the "rest" matches api.AppHostname , else we
170
+ // 6. Finally, we verify that the "rest" matches api.Hostname , else we
165
171
// return a 404.
166
172
//
167
173
// Rationales for each of the above steps:
168
- // 1. We pass on if api.AppHostname is not set to avoid returning any errors if
174
+ // 1. We pass on if api.Hostname is not set to avoid returning any errors if
169
175
// `--app-hostname` is not configured.
170
176
// 2. Every request should have a valid Host header anyways.
171
177
// 3. We pass on if the request hostname matches api.AccessURL so we can
@@ -175,22 +181,22 @@ func (s *WorkspaceAppServer) workspaceAppsProxyPath(rw http.ResponseWriter, r *h
175
181
// must be a subdomain of a hostname, which implies there must be at least
176
182
// one period.
177
183
// 5. a. If the request subdomain is not a valid application URL, and the
178
- // "rest" does not match api.AppHostname , then it is very unlikely that
184
+ // "rest" does not match api.Hostname , then it is very unlikely that
179
185
// the request was intended for this handler. We pass on.
180
186
// b. If the request subdomain is not a valid application URL, but the
181
- // "rest" matches api.AppHostname , then we return a 400 because the
187
+ // "rest" matches api.Hostname , then we return a 400 because the
182
188
// request is probably a typo or something.
183
- // 6. We finally verify that the "rest" matches api.AppHostname for security
189
+ // 6. We finally verify that the "rest" matches api.Hostname for security
184
190
// purposes regarding re-authentication and application proxy session
185
191
// tokens.
186
- func (s * WorkspaceAppServer )SubdomainAppMW (middlewares ... func (http.Handler ) http.Handler )func (http.Handler ) http.Handler {
192
+ func (s * Server )SubdomainAppMW (middlewares ... func (http.Handler ) http.Handler )func (http.Handler ) http.Handler {
187
193
return func (next http.Handler ) http.Handler {
188
194
return http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
189
195
ctx := r .Context ()
190
196
191
197
// Step 1: Pass on if subdomain-based application proxying is not
192
198
// configured.
193
- if s .AppHostname == "" || s .AppHostnameRegex == nil {
199
+ if s .Hostname == "" || s .HostnameRegex == nil {
194
200
next .ServeHTTP (rw ,r )
195
201
return
196
202
}
@@ -214,7 +220,7 @@ func (s *WorkspaceAppServer) SubdomainAppMW(middlewares ...func(http.Handler) ht
214
220
}
215
221
216
222
// Steps 3-6: Parse application from subdomain.
217
- app ,ok := s .parseWorkspaceApplicationHostname (rw ,r ,next ,host )
223
+ app ,ok := s .parseHostname (rw ,r ,next ,host )
218
224
if ! ok {
219
225
return
220
226
}
@@ -233,7 +239,7 @@ func (s *WorkspaceAppServer) SubdomainAppMW(middlewares ...func(http.Handler) ht
233
239
// Retry is disabled because the user needs to remove
234
240
// the query parameter before they try again.
235
241
RetryEnabled :false ,
236
- DashboardURL :s .AccessURL .String (),
242
+ DashboardURL :s .DashboardURL .String (),
237
243
})
238
244
return
239
245
}
@@ -256,7 +262,7 @@ func (s *WorkspaceAppServer) SubdomainAppMW(middlewares ...func(http.Handler) ht
256
262
return
257
263
}
258
264
259
- token ,ok := ResolveRequest (s .Logger ,s .AccessURL ,s .TokenProvider ,rw ,r ,Request {
265
+ token ,ok := ResolveRequest (s .Logger ,s .DashboardURL ,s .SignedTokenProvider ,rw ,r ,Request {
260
266
AccessMethod :AccessMethodSubdomain ,
261
267
BasePath :"/" ,
262
268
UsernameOrID :app .Username ,
@@ -278,11 +284,16 @@ func (s *WorkspaceAppServer) SubdomainAppMW(middlewares ...func(http.Handler) ht
278
284
}
279
285
}
280
286
281
- func (s * WorkspaceAppServer )parseWorkspaceApplicationHostname (rw http.ResponseWriter ,r * http.Request ,next http.Handler ,host string ) (httpapi.ApplicationURL ,bool ) {
287
+ // parseHostname will return if a given request is attempting to access a
288
+ // workspace app via a subdomain. If it is, the hostname of the request is parsed
289
+ // into an httpapi.ApplicationURL and true is returned. If the request is not
290
+ // accessing a workspace app, then the next handler is called and false is
291
+ // returned.
292
+ func (s * Server )parseHostname (rw http.ResponseWriter ,r * http.Request ,next http.Handler ,host string ) (httpapi.ApplicationURL ,bool ) {
282
293
// Check if the hostname matches either of the access URLs. If it does, the
283
294
// user was definitely trying to connect to the dashboard/API or a
284
295
// path-based app.
285
- if httpapi .HostnamesMatch (s .PrimaryAccessURL .Hostname (),host )|| httpapi .HostnamesMatch (s .AccessURL .Hostname (),host ) {
296
+ if httpapi .HostnamesMatch (s .DashboardURL .Hostname (),host )|| httpapi .HostnamesMatch (s .AccessURL .Hostname (),host ) {
286
297
next .ServeHTTP (rw ,r )
287
298
return httpapi.ApplicationURL {},false
288
299
}
@@ -296,7 +307,7 @@ func (s *WorkspaceAppServer) parseWorkspaceApplicationHostname(rw http.ResponseW
296
307
297
308
// Split the subdomain so we can parse the application details and verify it
298
309
// matches the configured app hostname later.
299
- subdomain ,ok := httpapi .ExecuteHostnamePattern (s .AppHostnameRegex ,host )
310
+ subdomain ,ok := httpapi .ExecuteHostnamePattern (s .HostnameRegex ,host )
300
311
if ! ok {
301
312
// Doesn't match the regex, so it's not a valid application URL.
302
313
next .ServeHTTP (rw ,r )
@@ -318,7 +329,7 @@ func (s *WorkspaceAppServer) parseWorkspaceApplicationHostname(rw http.ResponseW
318
329
Title :"Invalid Application URL" ,
319
330
Description :fmt .Sprintf ("Could not parse subdomain application URL %q: %s" ,subdomain ,err .Error ()),
320
331
RetryEnabled :false ,
321
- DashboardURL :s .AccessURL .String (),
332
+ DashboardURL :s .DashboardURL .String (),
322
333
})
323
334
return httpapi.ApplicationURL {},false
324
335
}
@@ -329,23 +340,23 @@ func (s *WorkspaceAppServer) parseWorkspaceApplicationHostname(rw http.ResponseW
329
340
// setWorkspaceAppCookie sets a cookie on the workspace app domain. If the app
330
341
// hostname cannot be parsed properly, a static error page is rendered and false
331
342
// is returned.
332
- func (s * WorkspaceAppServer )setWorkspaceAppCookie (rw http.ResponseWriter ,r * http.Request ,token string )bool {
333
- hostSplit := strings .SplitN (s .AppHostname ,"." ,2 )
343
+ func (s * Server )setWorkspaceAppCookie (rw http.ResponseWriter ,r * http.Request ,token string )bool {
344
+ hostSplit := strings .SplitN (s .Hostname ,"." ,2 )
334
345
if len (hostSplit )!= 2 {
335
346
// This should be impossible as we verify the app hostname on
336
347
// startup, but we'll check anyways.
337
- s .Logger .Error (r .Context (),"could not split invalid app hostname" ,slog .F ("hostname" ,s .AppHostname ))
348
+ s .Logger .Error (r .Context (),"could not split invalid app hostname" ,slog .F ("hostname" ,s .Hostname ))
338
349
site .RenderStaticErrorPage (rw ,r , site.ErrorPageData {
339
350
Status :http .StatusInternalServerError ,
340
351
Title :"Internal Server Error" ,
341
352
Description :"The app is configured with an invalid app wildcard hostname. Please contact an administrator." ,
342
353
RetryEnabled :false ,
343
- DashboardURL :s .AccessURL .String (),
354
+ DashboardURL :s .DashboardURL .String (),
344
355
})
345
356
return false
346
357
}
347
358
348
- // Set the app cookie for all subdomains of s.AppHostname . We don't set an
359
+ // Set the app cookie for all subdomains of s.Hostname . We don't set an
349
360
// expiration because the key in the database already has an expiration, and
350
361
// expired tokens don't affect the user experience (they get auto-redirected
351
362
// to re-smuggle the API key).
@@ -364,7 +375,7 @@ func (s *WorkspaceAppServer) setWorkspaceAppCookie(rw http.ResponseWriter, r *ht
364
375
return true
365
376
}
366
377
367
- func (s * WorkspaceAppServer )proxyWorkspaceApp (rw http.ResponseWriter ,r * http.Request ,appToken SignedToken ,path string ) {
378
+ func (s * Server )proxyWorkspaceApp (rw http.ResponseWriter ,r * http.Request ,appToken SignedToken ,path string ) {
368
379
ctx := r .Context ()
369
380
370
381
// Filter IP headers from untrusted origins.
@@ -384,7 +395,7 @@ func (s *WorkspaceAppServer) proxyWorkspaceApp(rw http.ResponseWriter, r *http.R
384
395
Title :"Bad Request" ,
385
396
Description :fmt .Sprintf ("Application has an invalid URL %q: %s" ,appToken .AppURL ,err .Error ()),
386
397
RetryEnabled :true ,
387
- DashboardURL :s .PrimaryAccessURL .String (),
398
+ DashboardURL :s .DashboardURL .String (),
388
399
})
389
400
return
390
401
}
@@ -439,7 +450,7 @@ func (s *WorkspaceAppServer) proxyWorkspaceApp(rw http.ResponseWriter, r *http.R
439
450
Title :"Bad Gateway" ,
440
451
Description :"Failed to proxy request to application: " + err .Error (),
441
452
RetryEnabled :true ,
442
- DashboardURL :s .PrimaryAccessURL .String (),
453
+ DashboardURL :s .DashboardURL .String (),
443
454
})
444
455
}
445
456
@@ -450,7 +461,7 @@ func (s *WorkspaceAppServer) proxyWorkspaceApp(rw http.ResponseWriter, r *http.R
450
461
Title :"Bad Gateway" ,
451
462
Description :"Could not connect to workspace agent: " + err .Error (),
452
463
RetryEnabled :true ,
453
- DashboardURL :s .PrimaryAccessURL .String (),
464
+ DashboardURL :s .DashboardURL .String (),
454
465
})
455
466
return
456
467
}
@@ -490,7 +501,7 @@ func (s *WorkspaceAppServer) proxyWorkspaceApp(rw http.ResponseWriter, r *http.R
490
501
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
491
502
// @Success 101
492
503
// @Router /workspaceagents/{workspaceagent}/pty [get]
493
- func (s * WorkspaceAppServer )workspaceAgentPTY (rw http.ResponseWriter ,r * http.Request ) {
504
+ func (s * Server )workspaceAgentPTY (rw http.ResponseWriter ,r * http.Request ) {
494
505
ctx := r .Context ()
495
506
496
507
// TODO: Fix this later
@@ -499,7 +510,7 @@ func (s *WorkspaceAppServer) workspaceAgentPTY(rw http.ResponseWriter, r *http.R
499
510
// s.WebsocketWaitMutex.Unlock()
500
511
// defer s.WebsocketWaitGroup.Done()
501
512
502
- appToken ,ok := ResolveRequest (s .Logger ,s .AccessURL ,s .TokenProvider ,rw ,r ,Request {
513
+ appToken ,ok := ResolveRequest (s .Logger ,s .AccessURL ,s .SignedTokenProvider ,rw ,r ,Request {
503
514
AccessMethod :AccessMethodTerminal ,
504
515
BasePath :r .URL .Path ,
505
516
AgentNameOrID :chi .URLParam (r ,"workspaceagent" ),