-
Notifications
You must be signed in to change notification settings - Fork 943
feat: Add serving applications on subdomains and port-based proxying #3753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
35ee5d6
a7e5bd6
03c697d
3e30cdd
8397306
f994ec3
fda80b8
6b09a0f
634cd2e
82df6f1
49084e2
4696bf9
b5d1f6a
0578588
77d3452
931ecb2
54f2bdd
f1d7670
46e0900
56c1d00
25d776a
f3c6645
75c4713
1f8a1f0
2c7bcc1
dc0d348
58653d4
5321bef
ad53b42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package coderd | |
|
||
import ( | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"net/http/httputil" | ||
"net/url" | ||
|
@@ -42,11 +43,12 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) | |
|
||
appName, port := AppNameOrPort(chi.URLParam(r, "workspaceapp")) | ||
api.proxyWorkspaceApplication(proxyApplication{ | ||
Workspace: workspace, | ||
Agent: agent, | ||
AppName: appName, | ||
Port: port, | ||
Path: chiPath, | ||
Workspace: workspace, | ||
Agent: agent, | ||
AppName: appName, | ||
Port: port, | ||
Path: chiPath, | ||
DashboardOnError: true, | ||
}, rw, r) | ||
} | ||
|
||
|
@@ -87,11 +89,12 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht | |
agent := httpmw.WorkspaceAgentParam(r) | ||
|
||
api.proxyWorkspaceApplication(proxyApplication{ | ||
Workspace: workspace, | ||
Agent: agent, | ||
AppName: app.AppName, | ||
Port: app.Port, | ||
Path: r.URL.Path, | ||
Workspace: workspace, | ||
Agent: agent, | ||
AppName: app.AppName, | ||
Port: app.Port, | ||
Path: r.URL.Path, | ||
DashboardOnError: false, | ||
}, rw, r) | ||
})).ServeHTTP(rw, r.WithContext(ctx)) | ||
}) | ||
|
@@ -108,6 +111,11 @@ type proxyApplication struct { | |
Port uint16 | ||
// Path must either be empty or have a leading slash. | ||
Path string | ||
|
||
// DashboardOnError determines whether or not the dashboard should be | ||
// rendered on error. This should be set for proxy path URLs but not | ||
// hostname based URLs. | ||
DashboardOnError bool | ||
} | ||
|
||
func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.ResponseWriter, r *http.Request) { | ||
|
@@ -178,14 +186,21 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res | |
|
||
proxy := httputil.NewSingleHostReverseProxy(appURL) | ||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { | ||
// This is a browser-facing route so JSON responses are not viable here. | ||
// To pass friendly errors to the frontend, special meta tags are overridden | ||
// in the index.html with the content passed here. | ||
r = r.WithContext(site.WithAPIResponse(r.Context(), site.APIResponse{ | ||
StatusCode: http.StatusBadGateway, | ||
Message: err.Error(), | ||
})) | ||
api.siteHandler.ServeHTTP(w, r) | ||
if proxyApp.DashboardOnError { | ||
deansheather marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// To pass friendly errors to the frontend, special meta tags are | ||
// overridden in the index.html with the content passed here. | ||
r = r.WithContext(site.WithAPIResponse(r.Context(), site.APIResponse{ | ||
StatusCode: http.StatusBadGateway, | ||
Message: err.Error(), | ||
})) | ||
api.siteHandler.ServeHTTP(w, r) | ||
return | ||
} | ||
|
||
httpapi.Write(w, http.StatusBadGateway, codersdk.Response{ | ||
Message: "Failed to proxy request to application.", | ||
Detail: err.Error(), | ||
}) | ||
} | ||
|
||
conn, release, err := api.workspaceAgentCache.Acquire(r, proxyApp.Agent.ID) | ||
|
@@ -234,6 +249,16 @@ type ApplicationURL struct { | |
BaseHostname string | ||
} | ||
|
||
// String returns the application URL hostname without scheme. | ||
func (a ApplicationURL) String() string { | ||
appNameOrPort := a.AppName | ||
if a.Port != 0 { | ||
appNameOrPort = strconv.Itoa(int(a.Port)) | ||
} | ||
|
||
return fmt.Sprintf("%s--%s--%s--%s.%s", appNameOrPort, a.AgentName, a.WorkspaceName, a.Username, a.BaseHostname) | ||
} | ||
|
||
// ParseSubdomainAppURL parses an application from the subdomain of r's Host | ||
// header. If the subdomain is not a valid application URL hostname, returns a | ||
// non-nil error. | ||
|
@@ -297,11 +322,17 @@ func AppNameOrPort(val string) (string, uint16) { | |
// only do application authentication, we will just reuse the original token. | ||
// This code should be temporary and be replaced with something that creates | ||
// a unique session_token. | ||
// | ||
// Returns nil if the access URL isn't a hostname. | ||
func (api *API) applicationCookie(authCookie *http.Cookie) *http.Cookie { | ||
if net.ParseIP(api.AccessURL.Hostname()) != nil { | ||
return nil | ||
} | ||
|
||
appCookie := *authCookie | ||
// We only support setting this cookie on the access url subdomains. | ||
// This is to ensure we don't accidentally leak the auth cookie to subdomains | ||
// on another hostname. | ||
// We only support setting this cookie on the access URL subdomains. This is | ||
// to ensure we don't accidentally leak the auth cookie to subdomains on | ||
// another hostname. | ||
appCookie.Domain = "." + api.AccessURL.Hostname() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting... I didn't know we could apply to subdomains without requiring an authentication redirect, this is awesome. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably won't be doing this soon as we'll be implementing proper devurl auth similarly to v1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kylecarbs this is a stop gap. It will not work on apps hosted on another domain. Also currently using the same full auth token. We should used scoped auth tokens. |
||
return &appCookie | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api.setAuthCookie