mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow prefixes at the beginning of subdomain app hostnames (#10150)
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -552,6 +553,118 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("WorkspaceAppsProxySubdomainHostnamePrefix/OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
appDetails := setupProxyTest(t, nil)
|
||||
|
||||
// Try to load the owner app with a prefix.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
prefixedOwnerApp := appDetails.Apps.Owner
|
||||
prefixedOwnerApp.Prefix = "some---prefix---"
|
||||
|
||||
u := appDetails.SubdomainAppURL(prefixedOwnerApp)
|
||||
require.Contains(t, u.Host, prefixedOwnerApp.Prefix)
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, resp.Header.Get("X-Got-Host"), u.Host)
|
||||
|
||||
// Parse the returned signed token to verify that it contains the
|
||||
// prefix.
|
||||
var appTokenCookie *http.Cookie
|
||||
for _, c := range resp.Cookies() {
|
||||
if c.Name == codersdk.SignedAppTokenCookie {
|
||||
appTokenCookie = c
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
|
||||
|
||||
// Parse the JWT without verifying it (since we can't access the key
|
||||
// from this test).
|
||||
object, err := jose.ParseSigned(appTokenCookie.Value)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, object.Signatures, 1)
|
||||
|
||||
// Parse the payload.
|
||||
var tok workspaceapps.SignedToken
|
||||
//nolint:gosec
|
||||
err = json.Unmarshal(object.UnsafePayloadWithoutVerification(), &tok)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the prefix is in the token.
|
||||
require.Equal(t, prefixedOwnerApp.Prefix, tok.Request.Prefix)
|
||||
|
||||
// Ensure the signed app token cookie is valid by making a request with
|
||||
// it with no session token.
|
||||
appTokenClient := appDetails.AppClient(t)
|
||||
appTokenClient.SetSessionToken("")
|
||||
appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})
|
||||
|
||||
resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, resp.Header.Get("X-Got-Host"), u.Host)
|
||||
})
|
||||
|
||||
t.Run("WorkspaceAppsProxySubdomainHostnamePrefix/Different", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
appDetails := setupProxyTest(t, nil)
|
||||
|
||||
// Try to load the owner app with a prefix.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
prefixedOwnerApp := appDetails.Apps.Owner
|
||||
t.Log(appDetails.SubdomainAppURL(prefixedOwnerApp))
|
||||
prefixedOwnerApp.Prefix = "some---prefix---"
|
||||
t.Log(appDetails.SubdomainAppURL(prefixedOwnerApp))
|
||||
|
||||
u := appDetails.SubdomainAppURL(prefixedOwnerApp)
|
||||
require.Contains(t, u.Host, prefixedOwnerApp.Prefix)
|
||||
|
||||
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// Find the cookie.
|
||||
var appTokenCookie *http.Cookie
|
||||
for _, c := range resp.Cookies() {
|
||||
if c.Name == codersdk.SignedAppTokenCookie {
|
||||
appTokenCookie = c
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
|
||||
|
||||
// Ensure the signed app token cookie is valid only for the given prefix
|
||||
// by making a request with it with no session token.
|
||||
appTokenClient := appDetails.AppClient(t)
|
||||
appTokenClient.SetSessionToken("")
|
||||
appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
|
||||
require.NoError(t, err)
|
||||
appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})
|
||||
|
||||
prefixedOwnerApp.Prefix = "different---"
|
||||
u = appDetails.SubdomainAppURL(prefixedOwnerApp)
|
||||
require.Contains(t, u.Host, prefixedOwnerApp.Prefix)
|
||||
|
||||
resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
_ = resp.Body.Close()
|
||||
require.NotEqual(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
// This test ensures that the subdomain handler does nothing if
|
||||
// --app-hostname is not set by the admin.
|
||||
t.Run("WorkspaceAppsProxySubdomainPassthrough", func(t *testing.T) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/workspaceapps"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
@@ -88,7 +89,9 @@ type App struct {
|
||||
AgentName string
|
||||
AppSlugOrPort string
|
||||
|
||||
Query string
|
||||
// Prefix should have ---.
|
||||
Prefix string
|
||||
Query string
|
||||
}
|
||||
|
||||
// Details are the full test details returned from setupProxyTestWithFactory.
|
||||
@@ -143,6 +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{
|
||||
Prefix: app.Prefix,
|
||||
AppSlugOrPort: app.AppSlugOrPort,
|
||||
AgentName: app.AgentName,
|
||||
WorkspaceName: app.WorkspaceName,
|
||||
@@ -252,6 +256,7 @@ func appServer(t *testing.T, headers http.Header, isHTTPS bool) uint16 {
|
||||
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||
assert.ErrorIs(t, err, http.ErrNoCookie)
|
||||
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
||||
w.Header().Set("X-Got-Host", r.Host)
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
w.Header().Add(name, value)
|
||||
@@ -290,6 +295,17 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Workspace name needs to be short to avoid hitting 62 char hostname
|
||||
// segment limit.
|
||||
workspaceName, err := cryptorand.String(6)
|
||||
require.NoError(t, err)
|
||||
workspaceName = "ws-" + workspaceName
|
||||
workspaceMutators = append([]func(*codersdk.CreateWorkspaceRequest){
|
||||
func(req *codersdk.CreateWorkspaceRequest) {
|
||||
req.Name = workspaceName
|
||||
},
|
||||
}, workspaceMutators...)
|
||||
|
||||
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
|
||||
protoApps := []*proto.App{
|
||||
{
|
||||
@@ -354,6 +370,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
require.True(t, app.Subdomain)
|
||||
|
||||
appURL := httpapi.ApplicationURL{
|
||||
Prefix: "",
|
||||
// findProtoApp is needed as the order of apps returned from PG database
|
||||
// is not guaranteed.
|
||||
AppSlugOrPort: findProtoApp(t, protoApps, app.Slug).Slug,
|
||||
@@ -382,6 +399,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
require.NoError(t, err)
|
||||
|
||||
appHost := httpapi.ApplicationURL{
|
||||
Prefix: "",
|
||||
AppSlugOrPort: "{{port}}",
|
||||
AgentName: proxyTestAgentName,
|
||||
WorkspaceName: workspace.Name,
|
||||
|
||||
Reference in New Issue
Block a user