Skip to content

Commit 35ee5d6

Browse files
committed
chore: Add subdomain parser for applications
1 parent c8f8c95 commit 35ee5d6

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

coderd/subdomain.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package coderd
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"regexp"
7+
"strings"
8+
9+
"golang.org/x/xerrors"
10+
)
11+
12+
const (
13+
// XForwardedHostHeader is a header used by proxies to indicate the
14+
// original host of the request.
15+
XForwardedHostHeader = "X-Forwarded-Host"
16+
xForwardedProto = "X-Forwarded-Proto"
17+
)
18+
19+
type Application struct {
20+
AppURL string
21+
AppName string
22+
Workspace string
23+
Agent string
24+
User string
25+
Path string
26+
27+
// Domain is used to output the url to reach the app.
28+
Domain string
29+
}
30+
31+
func (api *API) handleSubdomain(next http.Handler) http.Handler {
32+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
34+
})
35+
}
36+
37+
var (
38+
nameRegex = `[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*`
39+
appURL = regexp.MustCompile(fmt.Sprintf(
40+
// {USERNAME}--{WORKSPACE_NAME}}--{{AGENT_NAME}}--{{PORT}}
41+
`^(?P<UserName>%[1]s)--(?P<WorkspaceName>%[1]s)(--(?P<AgentName>%[1]s))?--(?P<AppName>%[1]s)$`,
42+
nameRegex))
43+
)
44+
45+
func ParseSubdomainAppURL(r *http.Request) (Application, error) {
46+
host := RequestHost(r)
47+
if host == "" {
48+
return Application{}, xerrors.Errorf("no host header")
49+
}
50+
51+
subdomain, domain, err := SplitSubdomain(host)
52+
if err != nil {
53+
return Application{}, xerrors.Errorf("split host domain: %w", err)
54+
}
55+
56+
matches := appURL.FindAllStringSubmatch(subdomain, -1)
57+
if len(matches) == 0 {
58+
return Application{}, xerrors.Errorf("invalid application url format: %q", subdomain)
59+
}
60+
61+
if len(matches) > 1 {
62+
return Application{}, xerrors.Errorf("multiple matches (%d) for application url: %q", len(matches), subdomain)
63+
}
64+
matchGroup := matches[0]
65+
66+
return Application{
67+
AppURL: "",
68+
AppName: matchGroup[appURL.SubexpIndex("AppName")],
69+
Workspace: matchGroup[appURL.SubexpIndex("WorkspaceName")],
70+
Agent: matchGroup[appURL.SubexpIndex("AgentName")],
71+
User: matchGroup[appURL.SubexpIndex("UserName")],
72+
Path: r.URL.Path,
73+
Domain: domain,
74+
}, nil
75+
}
76+
77+
// Parse parses a DevURL from the subdomain of r's Host header.
78+
// If DevURL is not valid, returns a non-nil error.
79+
//
80+
// devurls can be in two forms, each field separate by 2 hypthens:
81+
// 1) port-envname-user (eg. http://8080--myenv--johndoe.cdrdeploy.c8s.io)
82+
// 2) name-user (eg. http://demosvc--johndoe.cdrdeploy.c8s.io)
83+
//
84+
// Note that envname itself can contain hyphens.
85+
// If subdomain begins with a sequence of numbers, form 1 is assumed.
86+
// Otherwise, form 2 is assumed.
87+
//func Parse(r *http.Request, devurlSuffix string) (Application, error) {
88+
//
89+
// return d, nil
90+
//}
91+
92+
// RequestHost returns the name of the host from the request. It prioritizes
93+
// 'X-Forwarded-Host' over r.Host since most requests are being proxied.
94+
func RequestHost(r *http.Request) string {
95+
host := r.Header.Get(XForwardedHostHeader)
96+
if host != "" {
97+
return host
98+
}
99+
100+
return r.Host
101+
}
102+
103+
// SplitSubdomain splits a subdomain from a domain. E.g.:
104+
// - "foo.bar.com" becomes "foo", "bar.com"
105+
// - "foo.bar.baz.com" becomes "foo", "bar.baz.com"
106+
//
107+
// An error is returned if the string doesn't contain a period.
108+
func SplitSubdomain(hostname string) (string, string, error) {
109+
toks := strings.SplitN(hostname, ".", 2)
110+
if len(toks) < 2 {
111+
return "", "", xerrors.Errorf("no domain")
112+
}
113+
114+
return toks[0], toks[1], nil
115+
}

coderd/subdomain_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package coderd_test
2+
3+
import (
4+
"net/http/httptest"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/coderd"
10+
)
11+
12+
func TestParseSubdomainAppURL(t *testing.T) {
13+
t.Parallel()
14+
testCases := []struct {
15+
Name string
16+
URL string
17+
Expected coderd.Application
18+
ExpectedError string
19+
}{
20+
{
21+
Name: "Empty",
22+
URL: "https://example.com",
23+
Expected: coderd.Application{},
24+
ExpectedError: "invalid application url format",
25+
},
26+
{
27+
Name: "Workspace.Agent+App",
28+
URL: "https://workspace.agent--app.coder.com",
29+
Expected: coderd.Application{},
30+
ExpectedError: "invalid application url format",
31+
},
32+
{
33+
Name: "Workspace+App",
34+
URL: "https://workspace--app.coder.com",
35+
Expected: coderd.Application{},
36+
ExpectedError: "invalid application url format",
37+
},
38+
// Correct
39+
{
40+
Name: "User+Workspace+App",
41+
URL: "https://user--workspace--app.coder.com",
42+
Expected: coderd.Application{
43+
AppURL: "",
44+
AppName: "app",
45+
Workspace: "workspace",
46+
Agent: "",
47+
User: "user",
48+
Path: "",
49+
Domain: "coder.com",
50+
},
51+
},
52+
{
53+
Name: "User+Workspace+Port",
54+
URL: "https://user--workspace--8080.coder.com",
55+
Expected: coderd.Application{
56+
AppURL: "",
57+
AppName: "8080",
58+
Workspace: "workspace",
59+
Agent: "",
60+
User: "user",
61+
Path: "",
62+
Domain: "coder.com",
63+
},
64+
},
65+
{
66+
Name: "User+Workspace.Agent+App",
67+
URL: "https://user--workspace--agent--app.coder.com",
68+
Expected: coderd.Application{
69+
AppURL: "",
70+
AppName: "app",
71+
Workspace: "workspace",
72+
Agent: "agent",
73+
User: "user",
74+
Path: "",
75+
Domain: "coder.com",
76+
},
77+
},
78+
{
79+
Name: "User+Workspace.Agent+Port",
80+
URL: "https://user--workspace--agent--8080.coder.com",
81+
Expected: coderd.Application{
82+
AppURL: "",
83+
AppName: "8080",
84+
Workspace: "workspace",
85+
Agent: "agent",
86+
User: "user",
87+
Path: "",
88+
Domain: "coder.com",
89+
},
90+
},
91+
}
92+
93+
for _, c := range testCases {
94+
c := c
95+
t.Run(c.Name, func(t *testing.T) {
96+
t.Parallel()
97+
r := httptest.NewRequest("GET", c.URL, nil)
98+
99+
app, err := coderd.ParseSubdomainAppURL(r)
100+
if c.ExpectedError == "" {
101+
require.NoError(t, err)
102+
require.Equal(t, c.Expected, app, "expected app")
103+
} else {
104+
require.ErrorContains(t, err, c.ExpectedError, "expected error")
105+
}
106+
})
107+
}
108+
}

0 commit comments

Comments
 (0)