Skip to content

Commit 3e30cdd

Browse files
committed
Handle ports as app names
1 parent 03c697d commit 3e30cdd

File tree

4 files changed

+92
-79
lines changed

4 files changed

+92
-79
lines changed

coderd/coderd.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,13 @@ func New(options *Options) *API {
131131
})
132132
},
133133
httpmw.Prometheus(options.PrometheusRegistry),
134-
api.handleSubdomain,
134+
// Handle all subdomain requests
135+
api.handleSubdomain(
136+
httpmw.RateLimitPerMinute(options.APIRateLimit),
137+
httpmw.ExtractAPIKey(options.Database, oauthConfigs, false),
138+
httpmw.ExtractUserParam(api.Database),
139+
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
140+
),
135141
)
136142

137143
apps := func(r chi.Router) {

coderd/subdomain.go

+35-51
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
package coderd
22

33
import (
4-
"database/sql"
54
"fmt"
65
"net/http"
76
"regexp"
87
"strings"
98

10-
"github.com/coder/coder/coderd/httpapi"
11-
"github.com/coder/coder/codersdk"
9+
"github.com/coder/coder/coderd/httpmw"
1210

13-
"github.com/coder/coder/coderd/database"
11+
"github.com/go-chi/chi/v5"
1412

1513
"golang.org/x/xerrors"
1614
)
@@ -34,56 +32,42 @@ type Application struct {
3432
Domain string
3533
}
3634

37-
func (api *API) handleSubdomain(next http.Handler) http.Handler {
38-
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
39-
ctx := r.Context()
40-
app, err := ParseSubdomainAppURL(r)
41-
if err != nil {
42-
// Not a Dev URL, proceed as usual.
43-
// TODO: @emyrk we should probably catch invalid subdomains. Meaning
44-
// an invalid devurl should not route to the coderd.
45-
next.ServeHTTP(rw, r)
46-
return
47-
}
48-
49-
user, err := api.Database.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
50-
Username: app.Username,
51-
})
52-
if err != nil {
53-
if xerrors.Is(err, sql.ErrNoRows) {
54-
httpapi.ResourceNotFound(rw)
35+
func (api *API) handleSubdomain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
36+
return func(next http.Handler) http.Handler {
37+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
38+
ctx := r.Context()
39+
app, err := ParseSubdomainAppURL(r)
40+
41+
if err != nil {
42+
// Not a Dev URL, proceed as usual.
43+
// TODO: @emyrk we should probably catch invalid subdomains. Meaning
44+
// an invalid devurl should not route to the coderd.
45+
next.ServeHTTP(rw, r)
5546
return
5647
}
57-
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
58-
Message: "Internal error fetching user.",
59-
Detail: err.Error(),
60-
})
61-
return
62-
}
63-
64-
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
65-
OwnerID: user.ID,
66-
Name: app.WorkspaceName,
67-
})
68-
if err != nil {
69-
if xerrors.Is(err, sql.ErrNoRows) {
70-
httpapi.ResourceNotFound(rw)
71-
return
48+
49+
workspaceAgentKey := app.WorkspaceName
50+
if app.Agent != "" {
51+
workspaceAgentKey += "." + app.Agent
7252
}
73-
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
74-
Message: "Internal error fetching workspace.",
75-
Detail: err.Error(),
76-
})
77-
return
78-
}
79-
80-
api.proxyWorkspaceApplication(proxyApplication{
81-
Workspace: workspace,
82-
// TODO: Fetch workspace agent
83-
Agent: database.WorkspaceAgent{},
84-
AppName: app.AppName,
85-
}, rw, r)
86-
})
53+
chiCtx := chi.RouteContext(ctx)
54+
chiCtx.URLParams.Add("workspace_and_agent", workspaceAgentKey)
55+
chiCtx.URLParams.Add("user", app.Username)
56+
57+
// Use the passed in app middlewares before passing to the proxy app.
58+
mws := chi.Middlewares(middlewares)
59+
mws.Handler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
60+
workspace := httpmw.WorkspaceParam(r)
61+
agent := httpmw.WorkspaceAgentParam(r)
62+
63+
api.proxyWorkspaceApplication(proxyApplication{
64+
Workspace: workspace,
65+
Agent: agent,
66+
AppName: app.AppName,
67+
}, rw, r)
68+
})).ServeHTTP(rw, r.WithContext(ctx))
69+
})
70+
}
8771
}
8872

8973
var (

coderd/subdomain_test.go

+17-4
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
4444
AppName: "app",
4545
WorkspaceName: "workspace",
4646
Agent: "",
47-
User: "user",
47+
Username: "user",
4848
Path: "",
4949
Domain: "coder.com",
5050
},
@@ -57,7 +57,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
5757
AppName: "8080",
5858
WorkspaceName: "workspace",
5959
Agent: "",
60-
User: "user",
60+
Username: "user",
6161
Path: "",
6262
Domain: "coder.com",
6363
},
@@ -70,7 +70,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
7070
AppName: "app",
7171
WorkspaceName: "workspace",
7272
Agent: "agent",
73-
User: "user",
73+
Username: "user",
7474
Path: "",
7575
Domain: "coder.com",
7676
},
@@ -83,7 +83,20 @@ func TestParseSubdomainAppURL(t *testing.T) {
8383
AppName: "8080",
8484
WorkspaceName: "workspace",
8585
Agent: "agent",
86-
User: "user",
86+
Username: "user",
87+
Path: "",
88+
Domain: "coder.com",
89+
},
90+
},
91+
{
92+
Name: "HyphenatedNames",
93+
URL: "https://admin-user--workspace-thing--agent-thing--app-name.coder.com",
94+
Expected: coderd.Application{
95+
AppURL: "",
96+
AppName: "app-name",
97+
WorkspaceName: "workspace-thing",
98+
Agent: "agent-thing",
99+
Username: "admin-user",
87100
Path: "",
88101
Domain: "coder.com",
89102
},

coderd/workspaceapps.go

+33-23
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/http/httputil"
99
"net/url"
10+
"strconv"
1011
"strings"
1112

1213
"github.com/coder/coder/coderd/database"
@@ -51,34 +52,43 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
5152
return
5253
}
5354

54-
app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{
55-
AgentID: proxyApp.Agent.ID,
56-
Name: proxyApp.AppName,
57-
})
58-
if errors.Is(err, sql.ErrNoRows) {
59-
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
60-
Message: "Application not found.",
61-
})
62-
return
63-
}
64-
if err != nil {
65-
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
66-
Message: "Internal error fetching workspace application.",
67-
Detail: err.Error(),
68-
})
69-
return
70-
}
71-
if !app.Url.Valid {
72-
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
73-
Message: fmt.Sprintf("Application %s does not have a url.", app.Name),
55+
var internalURL string
56+
57+
num, err := strconv.Atoi(proxyApp.AppName)
58+
if err == nil && num <= 65535 {
59+
// TODO: @emyrk we should probably allow changing the schema?
60+
internalURL = "http://localhost:" + proxyApp.AppName
61+
} else {
62+
app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{
63+
AgentID: proxyApp.Agent.ID,
64+
Name: proxyApp.AppName,
7465
})
75-
return
66+
if errors.Is(err, sql.ErrNoRows) {
67+
httpapi.Write(rw, http.StatusNotFound, codersdk.Response{
68+
Message: "Application not found.",
69+
})
70+
return
71+
}
72+
if err != nil {
73+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
74+
Message: "Internal error fetching workspace application.",
75+
Detail: err.Error(),
76+
})
77+
return
78+
}
79+
if !app.Url.Valid {
80+
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
81+
Message: fmt.Sprintf("Application %s does not have a url.", app.Name),
82+
})
83+
return
84+
}
85+
internalURL = app.Url.String
7686
}
7787

78-
appURL, err := url.Parse(app.Url.String)
88+
appURL, err := url.Parse(internalURL)
7989
if err != nil {
8090
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
81-
Message: fmt.Sprintf("App url %q must be a valid url.", app.Url.String),
91+
Message: fmt.Sprintf("App url %q must be a valid url.", internalURL),
8292
Detail: err.Error(),
8393
})
8494
return

0 commit comments

Comments
 (0)