4
4
"bytes"
5
5
"context"
6
6
"crypto/sha256"
7
+ "database/sql"
7
8
"encoding/base64"
8
9
"encoding/json"
9
10
"fmt"
@@ -66,10 +67,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
66
67
Workspace : workspace ,
67
68
Agent : agent ,
68
69
// We do not support port proxying for paths.
69
- AppName : chi .URLParam (r , "workspaceapp" ),
70
- Port : 0 ,
71
- Path : chiPath ,
72
- DashboardOnError : true ,
70
+ AppName : chi .URLParam (r , "workspaceapp" ),
71
+ Port : 0 ,
72
+ Path : chiPath ,
73
73
}, rw , r )
74
74
}
75
75
@@ -162,33 +162,31 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht
162
162
}
163
163
164
164
api .proxyWorkspaceApplication (proxyApplication {
165
- Workspace : workspace ,
166
- Agent : agent ,
167
- AppName : app .AppName ,
168
- Port : app .Port ,
169
- Path : r .URL .Path ,
170
- DashboardOnError : false ,
165
+ Workspace : workspace ,
166
+ Agent : agent ,
167
+ AppName : app .AppName ,
168
+ Port : app .Port ,
169
+ Path : r .URL .Path ,
171
170
}, rw , r )
172
171
})).ServeHTTP (rw , r .WithContext (ctx ))
173
172
})
174
173
}
175
174
}
176
175
177
176
func (api * API ) parseWorkspaceApplicationHostname (rw http.ResponseWriter , r * http.Request , next http.Handler , host string ) (httpapi.ApplicationURL , bool ) {
178
- ctx := r .Context ()
179
- // Check if the hostname matches the access URL. If it does, the
180
- // user was definitely trying to connect to the dashboard/API.
177
+ // Check if the hostname matches the access URL. If it does, the user was
178
+ // definitely trying to connect to the dashboard/API.
181
179
if httpapi .HostnamesMatch (api .AccessURL .Hostname (), host ) {
182
180
next .ServeHTTP (rw , r )
183
181
return httpapi.ApplicationURL {}, false
184
182
}
185
183
186
- // Split the subdomain so we can parse the application details and
187
- // verify it matches the configured app hostname later.
184
+ // Split the subdomain so we can parse the application details and verify it
185
+ // matches the configured app hostname later.
188
186
subdomain , rest := httpapi .SplitSubdomain (host )
189
187
if rest == "" {
190
- // If there are no periods in the hostname, then it can't be a
191
- // valid application URL.
188
+ // If there are no periods in the hostname, then it can't be a valid
189
+ // application URL.
192
190
next .ServeHTTP (rw , r )
193
191
return httpapi.ApplicationURL {}, false
194
192
}
@@ -197,27 +195,34 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt
197
195
// Parse the application URL from the subdomain.
198
196
app , err := httpapi .ParseSubdomainAppURL (subdomain )
199
197
if err != nil {
200
- // If it isn't a valid app URL and the base domain doesn't match
201
- // the configured app hostname, this request was probably
202
- // destined for the dashboard/API router.
198
+ // If it isn't a valid app URL and the base domain doesn't match the
199
+ // configured app hostname, this request was probably destined for the
200
+ // dashboard/API router.
203
201
if ! matchingBaseHostname {
204
202
next .ServeHTTP (rw , r )
205
203
return httpapi.ApplicationURL {}, false
206
204
}
207
205
208
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
209
- Message : "Could not parse subdomain application URL." ,
210
- Detail : err .Error (),
206
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
207
+ Status : http .StatusBadRequest ,
208
+ Title : "Invalid application URL" ,
209
+ Description : fmt .Sprintf ("Could not parse subdomain application URL %q: %s" , subdomain , err .Error ()),
210
+ RetryEnabled : false ,
211
+ DashboardURL : api .AccessURL .String (),
211
212
})
212
213
return httpapi.ApplicationURL {}, false
213
214
}
214
215
215
- // At this point we've verified that the subdomain looks like a
216
- // valid application URL, so the base hostname should match the
217
- // configured app hostname.
216
+ // At this point we've verified that the subdomain looks like a valid
217
+ // application URL, so the base hostname should match the configured app
218
+ // hostname.
218
219
if ! matchingBaseHostname {
219
- httpapi .Write (ctx , rw , http .StatusNotFound , codersdk.Response {
220
- Message : "The server does not accept application requests on this hostname." ,
220
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
221
+ Status : http .StatusNotFound ,
222
+ Title : "Not Found" ,
223
+ Description : "The server does not accept application requests on this hostname." ,
224
+ RetryEnabled : false ,
225
+ DashboardURL : api .AccessURL .String (),
221
226
})
222
227
return httpapi.ApplicationURL {}, false
223
228
}
@@ -230,12 +235,10 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt
230
235
// they will be redirected to the route below. If the user does have a session
231
236
// key but insufficient permissions a static error page will be rendered.
232
237
func (api * API ) verifyWorkspaceApplicationAuth (rw http.ResponseWriter , r * http.Request , workspace database.Workspace , host string ) bool {
233
- ctx := r .Context ()
234
238
_ , ok := httpmw .APIKeyOptional (r )
235
239
if ok {
236
240
if ! api .Authorize (r , rbac .ActionCreate , workspace .ApplicationConnectRBAC ()) {
237
- // TODO: This should be a static error page.
238
- httpapi .ResourceNotFound (rw )
241
+ renderApplicationNotFound (rw , r , api .AccessURL )
239
242
return false
240
243
}
241
244
@@ -249,9 +252,14 @@ func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.R
249
252
// Exchange the encoded API key for a real one.
250
253
_ , apiKey , err := decryptAPIKey (r .Context (), api .Database , encryptedAPIKey )
251
254
if err != nil {
252
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
253
- Message : "Could not decrypt API key. Please remove the query parameter and try again." ,
254
- Detail : err .Error (),
255
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
256
+ Status : http .StatusBadRequest ,
257
+ Title : "Bad Request" ,
258
+ Description : "Could not decrypt API key. Please remove the query parameter and try again." ,
259
+ // Retry is disabled because the user needs to remove the query
260
+ // parameter before they try again.
261
+ RetryEnabled : false ,
262
+ DashboardURL : api .AccessURL .String (),
255
263
})
256
264
return false
257
265
}
@@ -302,6 +310,10 @@ func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.R
302
310
303
311
// workspaceApplicationAuth is an endpoint on the main router that handles
304
312
// redirects from the subdomain handler.
313
+ //
314
+ // This endpoint is under /api so we don't return the friendly error page here.
315
+ // Any errors on this endpoint should be errors that are unlikely to happen
316
+ // in production unless the user messes with the URL.
305
317
func (api * API ) workspaceApplicationAuth (rw http.ResponseWriter , r * http.Request ) {
306
318
ctx := r .Context ()
307
319
if api .AppHostname == "" {
@@ -413,11 +425,6 @@ type proxyApplication struct {
413
425
Port uint16
414
426
// Path must either be empty or have a leading slash.
415
427
Path string
416
-
417
- // DashboardOnError determines whether or not the dashboard should be
418
- // rendered on error. This should be set for proxy path URLs but not
419
- // hostname based URLs.
420
- DashboardOnError bool
421
428
}
422
429
423
430
func (api * API ) proxyWorkspaceApplication (proxyApp proxyApplication , rw http.ResponseWriter , r * http.Request ) {
@@ -439,17 +446,28 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
439
446
AgentID : proxyApp .Agent .ID ,
440
447
Name : proxyApp .AppName ,
441
448
})
449
+ if xerrors .Is (err , sql .ErrNoRows ) {
450
+ renderApplicationNotFound (rw , r , api .AccessURL )
451
+ return
452
+ }
442
453
if err != nil {
443
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
444
- Message : "Internal error fetching workspace application." ,
445
- Detail : err .Error (),
454
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
455
+ Status : http .StatusInternalServerError ,
456
+ Title : "Internal Server Error" ,
457
+ Description : "Could not fetch workspace application: " + err .Error (),
458
+ RetryEnabled : true ,
459
+ DashboardURL : api .AccessURL .String (),
446
460
})
447
461
return
448
462
}
449
463
450
464
if ! app .Url .Valid {
451
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
452
- Message : fmt .Sprintf ("Application %s does not have a url." , app .Name ),
465
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
466
+ Status : http .StatusBadRequest ,
467
+ Title : "Bad Request" ,
468
+ Description : fmt .Sprintf ("Application %q does not have a URL set." , app .Name ),
469
+ RetryEnabled : true ,
470
+ DashboardURL : api .AccessURL .String (),
453
471
})
454
472
return
455
473
}
@@ -458,9 +476,12 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
458
476
459
477
appURL , err := url .Parse (internalURL )
460
478
if err != nil {
461
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
462
- Message : fmt .Sprintf ("App URL %q is invalid." , internalURL ),
463
- Detail : err .Error (),
479
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
480
+ Status : http .StatusBadRequest ,
481
+ Title : "Bad Request" ,
482
+ Description : fmt .Sprintf ("Application has an invalid URL %q: %s" , internalURL , err .Error ()),
483
+ RetryEnabled : true ,
484
+ DashboardURL : api .AccessURL .String (),
464
485
})
465
486
return
466
487
}
@@ -489,28 +510,23 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
489
510
490
511
proxy := httputil .NewSingleHostReverseProxy (appURL )
491
512
proxy .ErrorHandler = func (w http.ResponseWriter , r * http.Request , err error ) {
492
- if proxyApp .DashboardOnError {
493
- // To pass friendly errors to the frontend, special meta tags are
494
- // overridden in the index.html with the content passed here.
495
- r = r .WithContext (site .WithAPIResponse (ctx , site.APIResponse {
496
- StatusCode : http .StatusBadGateway ,
497
- Message : err .Error (),
498
- }))
499
- api .siteHandler .ServeHTTP (w , r )
500
- return
501
- }
502
-
503
- httpapi .Write (ctx , w , http .StatusBadGateway , codersdk.Response {
504
- Message : "Failed to proxy request to application." ,
505
- Detail : err .Error (),
513
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
514
+ Status : http .StatusBadGateway ,
515
+ Title : "Bad Gateway" ,
516
+ Description : "Failed to proxy request to application: " + err .Error (),
517
+ RetryEnabled : true ,
518
+ DashboardURL : api .AccessURL .String (),
506
519
})
507
520
}
508
521
509
522
conn , release , err := api .workspaceAgentCache .Acquire (r , proxyApp .Agent .ID )
510
523
if err != nil {
511
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
512
- Message : "Failed to dial workspace agent." ,
513
- Detail : err .Error (),
524
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
525
+ Status : http .StatusBadGateway ,
526
+ Title : "Bad Gateway" ,
527
+ Description : "Could not connect to workspace agent: " + err .Error (),
528
+ RetryEnabled : true ,
529
+ DashboardURL : api .AccessURL .String (),
514
530
})
515
531
return
516
532
}
@@ -648,3 +664,15 @@ func decryptAPIKey(ctx context.Context, db database.Store, encryptedAPIKey strin
648
664
649
665
return key , payload .APIKey , nil
650
666
}
667
+
668
+ // renderApplicationNotFound should always be used when the app is not found or
669
+ // the current user doesn't have permission to access it.
670
+ func renderApplicationNotFound (rw http.ResponseWriter , r * http.Request , accessURL * url.URL ) {
671
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
672
+ Status : http .StatusNotFound ,
673
+ Title : "Application not found" ,
674
+ Description : "The application or workspace you are trying to access does not exist." ,
675
+ RetryEnabled : false ,
676
+ DashboardURL : accessURL .String (),
677
+ })
678
+ }
0 commit comments