fix: allow ports in wildcard url configuration (#11657)

* fix: allow ports in wildcard url configuration

This just forwards the port to the ui that generates urls.
Our existing parsing + regex already supported ports for
subdomain app requests.
This commit is contained in:
Steven Masley
2024-01-18 09:44:05 -06:00
committed by GitHub
parent 1f0e6ba6c6
commit 6bb1a34a37
12 changed files with 203 additions and 31 deletions
+32
View File
@@ -963,6 +963,38 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("WildcardPortOK", func(t *testing.T) {
t.Parallel()
// Manually specifying a port should override the access url port on
// the app host.
appDetails := setupProxyTest(t, &DeploymentOptions{
// Just throw both the wsproxy and primary to same url.
AppHost: "*.test.coder.com:4444",
PrimaryAppHost: "*.test.coder.com:4444",
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
t.Logf("url: %s", u)
require.Equal(t, "4444", u.Port(), "port should be 4444")
// Assert the api response the UI uses has the port.
apphost, err := appDetails.SDKClient.AppHost(ctx)
require.NoError(t, err)
require.Equal(t, "*.test.coder.com:4444", apphost.Host, "apphost has port")
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("SuffixWildcardNotMatch", func(t *testing.T) {
t.Parallel()
+2 -1
View File
@@ -47,6 +47,7 @@ const (
// DeploymentOptions are the options for creating a *Deployment with a
// DeploymentFactory.
type DeploymentOptions struct {
PrimaryAppHost string
AppHost string
DisablePathApps bool
DisableSubdomainApps bool
@@ -407,7 +408,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
Username: me.Username,
}
proxyURL := "http://" + appHost.String() + strings.ReplaceAll(primaryAppHost.Host, "*", "")
require.Equal(t, proxyURL, manifest.VSCodePortProxyURI)
require.Equal(t, manifest.VSCodePortProxyURI, proxyURL)
}
agentCloser := agent.New(agent.Options{
Client: agentClient,
+42 -3
View File
@@ -3,6 +3,7 @@ package appurl
import (
"fmt"
"net"
"net/url"
"regexp"
"strings"
@@ -20,6 +21,36 @@ var (
validHostnameLabelRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
)
// SubdomainAppHost returns the URL of the apphost for subdomain based apps.
// It will omit the scheme.
//
// Arguments:
// apphost: Expected to contain a wildcard, example: "*.coder.com"
// accessURL: The access url for the deployment.
//
// Returns:
// 'apphost:port'
//
// For backwards compatibility and for "accessurl=localhost:0" purposes, we need
// to use the port from the accessurl if the apphost doesn't have a port.
// If the user specifies a port in the apphost, we will use that port instead.
func SubdomainAppHost(apphost string, accessURL *url.URL) string {
if apphost == "" {
return ""
}
if apphost != "" && accessURL.Port() != "" {
// This should always parse if we prepend a scheme. We should add
// the access url port if the apphost doesn't have a port specified.
appHostU, err := url.Parse(fmt.Sprintf("https://%s", apphost))
if err != nil || (err == nil && appHostU.Port() == "") {
apphost += fmt.Sprintf(":%s", accessURL.Port())
}
}
return apphost
}
// ApplicationURL is a parsed application URL hostname.
type ApplicationURL struct {
Prefix string
@@ -140,9 +171,7 @@ func CompileHostnamePattern(pattern string) (*regexp.Regexp, error) {
if strings.Contains(pattern, "http:") || strings.Contains(pattern, "https:") {
return nil, xerrors.Errorf("hostname pattern must not contain a scheme: %q", pattern)
}
if strings.Contains(pattern, ":") {
return nil, xerrors.Errorf("hostname pattern must not contain a port: %q", pattern)
}
if strings.HasPrefix(pattern, ".") || strings.HasSuffix(pattern, ".") {
return nil, xerrors.Errorf("hostname pattern must not start or end with a period: %q", pattern)
}
@@ -155,6 +184,16 @@ func CompileHostnamePattern(pattern string) (*regexp.Regexp, error) {
if !strings.HasPrefix(pattern, "*") {
return nil, xerrors.Errorf("hostname pattern must only contain an asterisk at the beginning: %q", pattern)
}
// If there is a hostname:port, we only care about the hostname. For hostname
// pattern reasons, we do not actually care what port the client is requesting.
// Any port provided here is used for generating urls for the ui, not for
// validation.
hostname, _, err := net.SplitHostPort(pattern)
if err == nil {
pattern = hostname
}
for i, label := range strings.Split(pattern, ".") {
if i == 0 {
// We have to allow the asterisk to be a valid hostname label, so
+7 -5
View File
@@ -193,11 +193,6 @@ func TestCompileHostnamePattern(t *testing.T) {
pattern: "https://*.hi.com",
errorContains: "must not contain a scheme",
},
{
name: "Invalid_ContainsPort",
pattern: "*.hi.com:8080",
errorContains: "must not contain a port",
},
{
name: "Invalid_StartPeriod",
pattern: ".hi.com",
@@ -249,6 +244,13 @@ func TestCompileHostnamePattern(t *testing.T) {
errorContains: "contains invalid label",
},
{
name: "Valid_ContainsPort",
pattern: "*.hi.com:8080",
// Although a port is provided, the regex already matches any port.
// So it is ignored for validation purposes.
expectedRegex: `([^.]+)\.hi\.com`,
},
{
name: "Valid_Simple",
pattern: "*.hi",