mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
chore: move app URL parsing to its own package (#11651)
* chore: move app url parsing to it's own package
This commit is contained in:
@ -20,7 +20,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
)
|
||||
@ -108,7 +108,7 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest
|
||||
return nil, xerrors.Errorf("fetching workspace agent data: %w", err)
|
||||
}
|
||||
|
||||
appHost := httpapi.ApplicationURL{
|
||||
appHost := appurl.ApplicationURL{
|
||||
AppSlugOrPort: "{{port}}",
|
||||
AgentName: workspaceAgent.Name,
|
||||
WorkspaceName: workspace.Name,
|
||||
|
2
coderd/apidoc/docs.go
generated
2
coderd/apidoc/docs.go
generated
@ -9061,7 +9061,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"wildcard_access_url": {
|
||||
"$ref": "#/definitions/clibase.URL"
|
||||
"type": "string"
|
||||
},
|
||||
"write_config": {
|
||||
"type": "boolean"
|
||||
|
2
coderd/apidoc/swagger.json
generated
2
coderd/apidoc/swagger.json
generated
@ -8111,7 +8111,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"wildcard_access_url": {
|
||||
"$ref": "#/definitions/clibase.URL"
|
||||
"type": "string"
|
||||
},
|
||||
"write_config": {
|
||||
"type": "boolean"
|
||||
|
@ -96,7 +96,7 @@ type Options struct {
|
||||
// E.g. "*.apps.coder.com" or "*-apps.coder.com".
|
||||
AppHostname string
|
||||
// AppHostnameRegex contains the regex version of options.AppHostname as
|
||||
// generated by httpapi.CompileHostnamePattern(). It MUST be set if
|
||||
// generated by appurl.CompileHostnamePattern(). It MUST be set if
|
||||
// options.AppHostname is set.
|
||||
AppHostnameRegex *regexp.Regexp
|
||||
Logger slog.Logger
|
||||
|
@ -62,7 +62,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/gitsshkey"
|
||||
"github.com/coder/coder/v2/coderd/healthcheck"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/schedule"
|
||||
@ -71,6 +70,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/updatecheck"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/codersdk/drpc"
|
||||
@ -372,7 +372,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
var appHostnameRegex *regexp.Regexp
|
||||
if options.AppHostname != "" {
|
||||
var err error
|
||||
appHostnameRegex, err = httpapi.CompileHostnamePattern(options.AppHostname)
|
||||
appHostnameRegex, err = appurl.CompileHostnamePattern(options.AppHostname)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,9 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/parameter"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/tailnet"
|
||||
@ -381,7 +381,7 @@ func AppSubdomain(dbApp database.WorkspaceApp, agentName, workspaceName, ownerNa
|
||||
if appSlug == "" {
|
||||
appSlug = dbApp.DisplayName
|
||||
}
|
||||
return httpapi.ApplicationURL{
|
||||
return appurl.ApplicationURL{
|
||||
// We never generate URLs with a prefix. We only allow prefixes when
|
||||
// parsing URLs from the hostname. Users that want this feature can
|
||||
// write out their own URLs.
|
||||
|
@ -21,10 +21,10 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/regosql"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
)
|
||||
@ -4566,11 +4566,11 @@ func (q *FakeQuerier) GetWorkspaceProxyByHostname(_ context.Context, params data
|
||||
|
||||
// Compile the app hostname regex. This is slow sadly.
|
||||
if params.AllowWildcardHostname {
|
||||
wildcardRegexp, err := httpapi.CompileHostnamePattern(proxy.WildcardHostname)
|
||||
wildcardRegexp, err := appurl.CompileHostnamePattern(proxy.WildcardHostname)
|
||||
if err != nil {
|
||||
return database.WorkspaceProxy{}, xerrors.Errorf("compile hostname pattern %q for proxy %q (%s): %w", proxy.WildcardHostname, proxy.Name, proxy.ID.String(), err)
|
||||
}
|
||||
if _, ok := httpapi.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok {
|
||||
if _, ok := appurl.ExecuteHostnamePattern(wildcardRegexp, params.Hostname); ok {
|
||||
return proxy, nil
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/go-chi/cors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -44,18 +44,18 @@ func Cors(allowAll bool, origins ...string) func(next http.Handler) http.Handler
|
||||
})
|
||||
}
|
||||
|
||||
func WorkspaceAppCors(regex *regexp.Regexp, app httpapi.ApplicationURL) func(next http.Handler) http.Handler {
|
||||
func WorkspaceAppCors(regex *regexp.Regexp, app appurl.ApplicationURL) func(next http.Handler) http.Handler {
|
||||
return cors.Handler(cors.Options{
|
||||
AllowOriginFunc: func(r *http.Request, rawOrigin string) bool {
|
||||
origin, err := url.Parse(rawOrigin)
|
||||
if rawOrigin == "" || origin.Host == "" || err != nil {
|
||||
return false
|
||||
}
|
||||
subdomain, ok := httpapi.ExecuteHostnamePattern(regex, origin.Host)
|
||||
subdomain, ok := appurl.ExecuteHostnamePattern(regex, origin.Host)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
originApp, err := httpapi.ParseSubdomainAppURL(subdomain)
|
||||
originApp, err := appurl.ParseSubdomainAppURL(subdomain)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
@ -7,14 +7,14 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
)
|
||||
|
||||
func TestWorkspaceAppCors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
regex, err := httpapi.CompileHostnamePattern("*--apps.dev.coder.com")
|
||||
regex, err := appurl.CompileHostnamePattern("*--apps.dev.coder.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
methods := []string{
|
||||
@ -30,13 +30,13 @@ func TestWorkspaceAppCors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
origin string
|
||||
app httpapi.ApplicationURL
|
||||
app appurl.ApplicationURL
|
||||
allowed bool
|
||||
}{
|
||||
{
|
||||
name: "Self",
|
||||
origin: "https://3000--agent--ws--user--apps.dev.coder.com",
|
||||
app: httpapi.ApplicationURL{
|
||||
app: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "3000",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "ws",
|
||||
@ -47,7 +47,7 @@ func TestWorkspaceAppCors(t *testing.T) {
|
||||
{
|
||||
name: "SameWorkspace",
|
||||
origin: "https://8000--agent--ws--user--apps.dev.coder.com",
|
||||
app: httpapi.ApplicationURL{
|
||||
app: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "3000",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "ws",
|
||||
@ -58,7 +58,7 @@ func TestWorkspaceAppCors(t *testing.T) {
|
||||
{
|
||||
name: "SameUser",
|
||||
origin: "https://8000--agent2--ws2--user--apps.dev.coder.com",
|
||||
app: httpapi.ApplicationURL{
|
||||
app: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "3000",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "ws",
|
||||
@ -69,7 +69,7 @@ func TestWorkspaceAppCors(t *testing.T) {
|
||||
{
|
||||
name: "DifferentOriginOwner",
|
||||
origin: "https://3000--agent--ws--user2--apps.dev.coder.com",
|
||||
app: httpapi.ApplicationURL{
|
||||
app: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "3000",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "ws",
|
||||
@ -80,7 +80,7 @@ func TestWorkspaceAppCors(t *testing.T) {
|
||||
{
|
||||
name: "DifferentHostOwner",
|
||||
origin: "https://3000--agent--ws--user--apps.dev.coder.com",
|
||||
app: httpapi.ApplicationURL{
|
||||
app: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "3000",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "ws",
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@ -169,7 +170,7 @@ func (api *API) ValidWorkspaceAppHostname(ctx context.Context, host string, opts
|
||||
}
|
||||
|
||||
if opts.AllowPrimaryWildcard && api.AppHostnameRegex != nil {
|
||||
_, ok := httpapi.ExecuteHostnamePattern(api.AppHostnameRegex, host)
|
||||
_, ok := appurl.ExecuteHostnamePattern(api.AppHostnameRegex, host)
|
||||
if ok {
|
||||
// Force the redirect URI to have the same scheme as the access URL
|
||||
// for security purposes.
|
||||
|
@ -21,8 +21,8 @@ import (
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/agent"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
@ -146,7 +146,7 @@ func (d *Details) PathAppURL(app App) *url.URL {
|
||||
|
||||
// SubdomainAppURL returns the URL for the given subdomain app.
|
||||
func (d *Details) SubdomainAppURL(app App) *url.URL {
|
||||
appHost := httpapi.ApplicationURL{
|
||||
appHost := appurl.ApplicationURL{
|
||||
Prefix: app.Prefix,
|
||||
AppSlugOrPort: app.AppSlugOrPort,
|
||||
AgentName: app.AgentName,
|
||||
@ -370,7 +370,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
for _, app := range workspaceBuild.Resources[0].Agents[0].Apps {
|
||||
require.True(t, app.Subdomain)
|
||||
|
||||
appURL := httpapi.ApplicationURL{
|
||||
appURL := appurl.ApplicationURL{
|
||||
Prefix: "",
|
||||
// findProtoApp is needed as the order of apps returned from PG database
|
||||
// is not guaranteed.
|
||||
@ -399,7 +399,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
manifest, err := agentClient.Manifest(appHostCtx)
|
||||
require.NoError(t, err)
|
||||
|
||||
appHost := httpapi.ApplicationURL{
|
||||
appHost := appurl.ApplicationURL{
|
||||
Prefix: "",
|
||||
AppSlugOrPort: "{{port}}",
|
||||
AgentName: proxyTestAgentName,
|
||||
|
@ -1,4 +1,4 @@
|
||||
package httpapi
|
||||
package appurl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -10,8 +10,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Remove the "starts with" and "ends with" regex components.
|
||||
nameRegex = strings.Trim(UsernameValidRegex.String(), "^$")
|
||||
// nameRegex is the same as our UsernameRegex without the ^ and $.
|
||||
nameRegex = "[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*"
|
||||
appURL = regexp.MustCompile(fmt.Sprintf(
|
||||
// {PORT/APP_SLUG}--{AGENT_NAME}--{WORKSPACE_NAME}--{USERNAME}
|
||||
`^(?P<AppSlug>%[1]s)--(?P<AgentName>%[1]s)--(?P<WorkspaceName>%[1]s)--(?P<Username>%[1]s)$`,
|
||||
@ -44,6 +44,14 @@ func (a ApplicationURL) String() string {
|
||||
return appURL.String()
|
||||
}
|
||||
|
||||
// Path is a helper function to get the url path of the app if it is not served
|
||||
// on a subdomain. In practice this is not really used because we use the chi
|
||||
// `{variable}` syntax to extract these parts. For testing purposes and for
|
||||
// completeness of this package, we include it.
|
||||
func (a ApplicationURL) Path() string {
|
||||
return fmt.Sprintf("/@%s/%s.%s/apps/%s", a.Username, a.WorkspaceName, a.AgentName, a.AppSlugOrPort)
|
||||
}
|
||||
|
||||
// ParseSubdomainAppURL parses an ApplicationURL from the given subdomain. If
|
||||
// the subdomain is not a valid application URL hostname, returns a non-nil
|
||||
// error. If the hostname is not a subdomain of the given base hostname, returns
|
@ -1,4 +1,4 @@
|
||||
package httpapi_test
|
||||
package appurl_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
)
|
||||
|
||||
func TestApplicationURLString(t *testing.T) {
|
||||
@ -14,17 +14,17 @@ func TestApplicationURLString(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
URL httpapi.ApplicationURL
|
||||
URL appurl.ApplicationURL
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Name: "Empty",
|
||||
URL: httpapi.ApplicationURL{},
|
||||
URL: appurl.ApplicationURL{},
|
||||
Expected: "------",
|
||||
},
|
||||
{
|
||||
Name: "AppName",
|
||||
URL: httpapi.ApplicationURL{
|
||||
URL: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "app",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -34,7 +34,7 @@ func TestApplicationURLString(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "Port",
|
||||
URL: httpapi.ApplicationURL{
|
||||
URL: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "8080",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -44,7 +44,7 @@ func TestApplicationURLString(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Name: "Prefix",
|
||||
URL: httpapi.ApplicationURL{
|
||||
URL: appurl.ApplicationURL{
|
||||
Prefix: "yolo---",
|
||||
AppSlugOrPort: "app",
|
||||
AgentName: "agent",
|
||||
@ -70,44 +70,44 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
Subdomain string
|
||||
Expected httpapi.ApplicationURL
|
||||
Expected appurl.ApplicationURL
|
||||
ExpectedError string
|
||||
}{
|
||||
{
|
||||
Name: "Invalid_Empty",
|
||||
Subdomain: "test",
|
||||
Expected: httpapi.ApplicationURL{},
|
||||
Expected: appurl.ApplicationURL{},
|
||||
ExpectedError: "invalid application url format",
|
||||
},
|
||||
{
|
||||
Name: "Invalid_Workspace.Agent--App",
|
||||
Subdomain: "workspace.agent--app",
|
||||
Expected: httpapi.ApplicationURL{},
|
||||
Expected: appurl.ApplicationURL{},
|
||||
ExpectedError: "invalid application url format",
|
||||
},
|
||||
{
|
||||
Name: "Invalid_Workspace--App",
|
||||
Subdomain: "workspace--app",
|
||||
Expected: httpapi.ApplicationURL{},
|
||||
Expected: appurl.ApplicationURL{},
|
||||
ExpectedError: "invalid application url format",
|
||||
},
|
||||
{
|
||||
Name: "Invalid_App--Workspace--User",
|
||||
Subdomain: "app--workspace--user",
|
||||
Expected: httpapi.ApplicationURL{},
|
||||
Expected: appurl.ApplicationURL{},
|
||||
ExpectedError: "invalid application url format",
|
||||
},
|
||||
{
|
||||
Name: "Invalid_TooManyComponents",
|
||||
Subdomain: "1--2--3--4--5",
|
||||
Expected: httpapi.ApplicationURL{},
|
||||
Expected: appurl.ApplicationURL{},
|
||||
ExpectedError: "invalid application url format",
|
||||
},
|
||||
// Correct
|
||||
{
|
||||
Name: "AppName--Agent--Workspace--User",
|
||||
Subdomain: "app--agent--workspace--user",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
Expected: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "app",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -117,7 +117,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
{
|
||||
Name: "Port--Agent--Workspace--User",
|
||||
Subdomain: "8080--agent--workspace--user",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
Expected: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "8080",
|
||||
AgentName: "agent",
|
||||
WorkspaceName: "workspace",
|
||||
@ -127,7 +127,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
{
|
||||
Name: "HyphenatedNames",
|
||||
Subdomain: "app-slug--agent-name--workspace-name--user-name",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
Expected: appurl.ApplicationURL{
|
||||
AppSlugOrPort: "app-slug",
|
||||
AgentName: "agent-name",
|
||||
WorkspaceName: "workspace-name",
|
||||
@ -137,7 +137,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
{
|
||||
Name: "Prefix",
|
||||
Subdomain: "dean---was---here---app--agent--workspace--user",
|
||||
Expected: httpapi.ApplicationURL{
|
||||
Expected: appurl.ApplicationURL{
|
||||
Prefix: "dean---was---here---",
|
||||
AppSlugOrPort: "app",
|
||||
AgentName: "agent",
|
||||
@ -152,7 +152,7 @@ func TestParseSubdomainAppURL(t *testing.T) {
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, err := httpapi.ParseSubdomainAppURL(c.Subdomain)
|
||||
app, err := appurl.ParseSubdomainAppURL(c.Subdomain)
|
||||
if c.ExpectedError == "" {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.Expected, app, "expected app")
|
||||
@ -370,7 +370,7 @@ func TestCompileHostnamePattern(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
regex, err := httpapi.CompileHostnamePattern(c.pattern)
|
||||
regex, err := appurl.CompileHostnamePattern(c.pattern)
|
||||
if c.errorContains == "" {
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -382,7 +382,7 @@ func TestCompileHostnamePattern(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("MatchCase%d", i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
match, ok := httpapi.ExecuteHostnamePattern(regex, m.input)
|
||||
match, ok := appurl.ExecuteHostnamePattern(regex, m.input)
|
||||
if m.match == "" {
|
||||
require.False(t, ok)
|
||||
} else {
|
2
coderd/workspaceapps/appurl/doc.go
Normal file
2
coderd/workspaceapps/appurl/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package appurl handles all parsing/validation/etc around application URLs.
|
||||
package appurl
|
@ -19,9 +19,9 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
@ -751,7 +751,7 @@ func Test_ResolveRequest(t *testing.T) {
|
||||
redirectURI, err := url.Parse(redirectURIStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
appHost := httpapi.ApplicationURL{
|
||||
appHost := appurl.ApplicationURL{
|
||||
Prefix: "",
|
||||
AppSlugOrPort: req.AppSlugOrPort,
|
||||
AgentName: req.AgentNameOrID,
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/site"
|
||||
)
|
||||
@ -96,7 +97,7 @@ type Server struct {
|
||||
// E.g. "*.apps.coder.com" or "*-apps.coder.com".
|
||||
Hostname string
|
||||
// HostnameRegex contains the regex version of Hostname as generated by
|
||||
// httpapi.CompileHostnamePattern(). It MUST be set if Hostname is set.
|
||||
// appurl.CompileHostnamePattern(). It MUST be set if Hostname is set.
|
||||
HostnameRegex *regexp.Regexp
|
||||
RealIPConfig *httpmw.RealIPConfig
|
||||
|
||||
@ -329,7 +330,7 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
||||
// 3. If the request hostname matches api.AccessURL then we pass on.
|
||||
// 5. We split the subdomain into the subdomain and the "rest". If there are no
|
||||
// periods in the hostname then we pass on.
|
||||
// 5. We parse the subdomain into a httpapi.ApplicationURL struct. If we
|
||||
// 5. We parse the subdomain into a appurl.ApplicationURL struct. If we
|
||||
// encounter an error:
|
||||
// a. If the "rest" does not match api.Hostname then we pass on;
|
||||
// b. Otherwise, we return a 400.
|
||||
@ -428,43 +429,43 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler)
|
||||
|
||||
// parseHostname will return if a given request is attempting to access a
|
||||
// workspace app via a subdomain. If it is, the hostname of the request is parsed
|
||||
// into an httpapi.ApplicationURL and true is returned. If the request is not
|
||||
// into an appurl.ApplicationURL and true is returned. If the request is not
|
||||
// accessing a workspace app, then the next handler is called and false is
|
||||
// returned.
|
||||
func (s *Server) parseHostname(rw http.ResponseWriter, r *http.Request, next http.Handler, host string) (httpapi.ApplicationURL, bool) {
|
||||
func (s *Server) parseHostname(rw http.ResponseWriter, r *http.Request, next http.Handler, host string) (appurl.ApplicationURL, bool) {
|
||||
// Check if the hostname matches either of the access URLs. If it does, the
|
||||
// user was definitely trying to connect to the dashboard/API or a
|
||||
// path-based app.
|
||||
if httpapi.HostnamesMatch(s.DashboardURL.Hostname(), host) || httpapi.HostnamesMatch(s.AccessURL.Hostname(), host) {
|
||||
if appurl.HostnamesMatch(s.DashboardURL.Hostname(), host) || appurl.HostnamesMatch(s.AccessURL.Hostname(), host) {
|
||||
next.ServeHTTP(rw, r)
|
||||
return httpapi.ApplicationURL{}, false
|
||||
return appurl.ApplicationURL{}, false
|
||||
}
|
||||
|
||||
// If there are no periods in the hostname, then it can't be a valid
|
||||
// application URL.
|
||||
if !strings.Contains(host, ".") {
|
||||
next.ServeHTTP(rw, r)
|
||||
return httpapi.ApplicationURL{}, false
|
||||
return appurl.ApplicationURL{}, false
|
||||
}
|
||||
|
||||
// Split the subdomain so we can parse the application details and verify it
|
||||
// matches the configured app hostname later.
|
||||
subdomain, ok := httpapi.ExecuteHostnamePattern(s.HostnameRegex, host)
|
||||
subdomain, ok := appurl.ExecuteHostnamePattern(s.HostnameRegex, host)
|
||||
if !ok {
|
||||
// Doesn't match the regex, so it's not a valid application URL.
|
||||
next.ServeHTTP(rw, r)
|
||||
return httpapi.ApplicationURL{}, false
|
||||
return appurl.ApplicationURL{}, false
|
||||
}
|
||||
|
||||
// Check if the request is part of the deprecated logout flow. If so, we
|
||||
// just redirect to the main access URL.
|
||||
if subdomain == appLogoutHostname {
|
||||
http.Redirect(rw, r, s.AccessURL.String(), http.StatusSeeOther)
|
||||
return httpapi.ApplicationURL{}, false
|
||||
return appurl.ApplicationURL{}, false
|
||||
}
|
||||
|
||||
// Parse the application URL from the subdomain.
|
||||
app, err := httpapi.ParseSubdomainAppURL(subdomain)
|
||||
app, err := appurl.ParseSubdomainAppURL(subdomain)
|
||||
if err != nil {
|
||||
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
|
||||
Status: http.StatusBadRequest,
|
||||
@ -473,7 +474,7 @@ func (s *Server) parseHostname(rw http.ResponseWriter, r *http.Request, next htt
|
||||
RetryEnabled: false,
|
||||
DashboardURL: s.DashboardURL.String(),
|
||||
})
|
||||
return httpapi.ApplicationURL{}, false
|
||||
return appurl.ApplicationURL{}, false
|
||||
}
|
||||
|
||||
return app, true
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@ -63,7 +63,7 @@ func (r IssueTokenRequest) AppBaseURL() (*url.URL, error) {
|
||||
return nil, xerrors.New("subdomain app hostname is required to generate subdomain app URL")
|
||||
}
|
||||
|
||||
appHost := httpapi.ApplicationURL{
|
||||
appHost := appurl.ApplicationURL{
|
||||
Prefix: r.AppRequest.Prefix,
|
||||
AppSlugOrPort: r.AppRequest.AppSlugOrPort,
|
||||
AgentName: r.AppRequest.AgentNameOrID,
|
||||
|
Reference in New Issue
Block a user