feat: Add serving applications on subdomains and port-based proxying (#3753)

Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
Steven Masley
2022-09-13 13:31:33 -04:00
committed by GitHub
parent 99a7a8dd22
commit 9ab437d6e2
16 changed files with 895 additions and 88 deletions

View File

@ -6,6 +6,7 @@ import (
"io"
"net"
"net/http"
"net/url"
"testing"
"time"
@ -16,14 +17,26 @@ import (
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil"
)
func TestWorkspaceAppsProxyPath(t *testing.T) {
t.Parallel()
const (
proxyTestAgentName = "agent-name"
proxyTestAppName = "example"
proxyTestAppQuery = "query=true"
proxyTestAppBody = "hello world"
proxyTestFakeAppName = "fake"
)
// setupProxyTest creates a workspace with an agent and some apps. It returns a
// codersdk client, the workspace, and the port number the test listener is
// running on.
func setupProxyTest(t *testing.T) (*codersdk.Client, uuid.UUID, codersdk.Workspace, uint16) {
// #nosec
ln, err := net.Listen("tcp", ":0")
require.NoError(t, err)
@ -33,6 +46,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
_, err := r.Cookie(codersdk.SessionTokenKey)
assert.ErrorIs(t, err, http.ErrNoCookie)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(proxyTestAppBody))
}),
}
t.Cleanup(func() {
@ -40,7 +54,8 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
_ = ln.Close()
})
go server.Serve(ln)
tcpAddr, _ := ln.Addr().(*net.TCPAddr)
tcpAddr, ok := ln.Addr().(*net.TCPAddr)
require.True(t, ok)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
@ -57,17 +72,21 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Id: uuid.NewString(),
Name: proxyTestAgentName,
Auth: &proto.Agent_Token{
Token: authToken,
},
Apps: []*proto.App{{
Name: "example",
Url: fmt.Sprintf("http://127.0.0.1:%d?query=true", tcpAddr.Port),
}, {
Name: "fake",
Url: "http://127.0.0.2",
}},
Apps: []*proto.App{
{
Name: proxyTestAppName,
Url: fmt.Sprintf("http://127.0.0.1:%d?%s", tcpAddr.Port, proxyTestAppQuery),
}, {
Name: proxyTestFakeAppName,
// Hopefully this IP and port doesn't exist.
Url: "http://127.1.0.1:65535",
},
},
}},
}},
},
@ -91,11 +110,28 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
_ = agentCloser.Close()
})
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
// Configure the HTTP client to not follow redirects and to route all
// requests regardless of hostname to the coderd test server.
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
require.True(t, ok)
transport := defaultTransport.Clone()
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, client.URL.Host)
}
client.HTTPClient.Transport = transport
t.Run("RedirectsWithoutAuth", func(t *testing.T) {
return client, user.OrganizationID, workspace, uint16(tcpAddr.Port)
}
func TestWorkspaceAppsProxyPath(t *testing.T) {
t.Parallel()
client, orgID, workspace, port := setupProxyTest(t)
t.Run("LoginWithoutAuth", func(t *testing.T) {
t.Parallel()
client := codersdk.New(client.URL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
@ -108,10 +144,27 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil)
require.NoError(t, err)
defer resp.Body.Close()
location, err := resp.Location()
loc, err := resp.Location()
require.NoError(t, err)
require.Equal(t, "/login", location.Path)
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.True(t, loc.Query().Has("message"))
require.True(t, loc.Query().Has("redirect"))
})
t.Run("NoAccessShould404", func(t *testing.T) {
t.Parallel()
userClient := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleMember())
userClient.HTTPClient.CheckRedirect = client.HTTPClient.CheckRedirect
userClient.HTTPClient.Transport = client.HTTPClient.Transport
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := userClient.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example", nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("RedirectsWithSlash", func(t *testing.T) {
@ -138,7 +191,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
loc, err := resp.Location()
require.NoError(t, err)
require.Equal(t, "query=true", loc.RawQuery)
require.Equal(t, proxyTestAppQuery, loc.RawQuery)
})
t.Run("Proxies", func(t *testing.T) {
@ -147,12 +200,28 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?query=true", nil)
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?"+proxyTestAppQuery, nil)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "", string(body))
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("ProxiesPort", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
path := fmt.Sprintf("/@me/%s/apps/%d/?%s", workspace.Name, port, proxyTestAppQuery)
resp, err := client.Request(ctx, http.MethodGet, path, 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)
})
@ -165,6 +234,174 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil)
require.NoError(t, err)
defer resp.Body.Close()
// this is 200 OK because it returns a dashboard page
require.Equal(t, http.StatusOK, resp.StatusCode)
})
}
func TestWorkspaceAppsProxySubdomain(t *testing.T) {
t.Parallel()
client, orgID, workspace, port := setupProxyTest(t)
// proxyURL generates a URL for the proxy subdomain. The default path is a
// slash.
proxyURL := func(t *testing.T, appNameOrPort interface{}, pathAndQuery ...string) string {
t.Helper()
var (
appName string
port uint16
)
if val, ok := appNameOrPort.(string); ok {
appName = val
} else {
port, ok = appNameOrPort.(uint16)
require.True(t, ok)
}
me, err := client.User(context.Background(), codersdk.Me)
require.NoError(t, err, "get current user details")
hostname := httpapi.ApplicationURL{
AppName: appName,
Port: port,
AgentName: proxyTestAgentName,
WorkspaceName: workspace.Name,
Username: me.Username,
BaseHostname: "test.coder.com",
}.String()
actualPath := "/"
query := ""
if len(pathAndQuery) > 0 {
actualPath = pathAndQuery[0]
}
if len(pathAndQuery) > 1 {
query = pathAndQuery[1]
}
return (&url.URL{
Scheme: "http",
Host: hostname,
Path: actualPath,
RawQuery: query,
}).String()
}
t.Run("LoginWithoutAuth", func(t *testing.T) {
t.Parallel()
unauthedClient := codersdk.New(client.URL)
unauthedClient.HTTPClient.CheckRedirect = client.HTTPClient.CheckRedirect
unauthedClient.HTTPClient.Transport = client.HTTPClient.Transport
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := unauthedClient.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName), nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
loc, err := resp.Location()
require.NoError(t, err)
require.True(t, loc.Query().Has("message"))
require.False(t, loc.Query().Has("redirect"))
expectedURL := *client.URL
expectedURL.Path = "/login"
loc.RawQuery = ""
require.Equal(t, &expectedURL, loc)
})
t.Run("NoAccessShould401", func(t *testing.T) {
t.Parallel()
userClient := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleMember())
userClient.HTTPClient.CheckRedirect = client.HTTPClient.CheckRedirect
userClient.HTTPClient.Transport = client.HTTPClient.Transport
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := userClient.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName), nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("RedirectsWithSlash", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
slashlessURL := proxyURL(t, proxyTestAppName, "")
resp, err := client.Request(ctx, http.MethodGet, slashlessURL, nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
loc, err := resp.Location()
require.NoError(t, err)
require.Equal(t, slashlessURL+"/?"+proxyTestAppQuery, loc.String())
})
t.Run("RedirectsWithQuery", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
querylessURL := proxyURL(t, proxyTestAppName, "/", "")
resp, err := client.Request(ctx, http.MethodGet, querylessURL, nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
loc, err := resp.Location()
require.NoError(t, err)
require.Equal(t, proxyTestAppQuery, loc.RawQuery)
})
t.Run("Proxies", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestAppName, "/", proxyTestAppQuery), 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("ProxiesPort", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, port, "/", proxyTestAppQuery), 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("ProxyError", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
resp, err := client.Request(ctx, http.MethodGet, proxyURL(t, proxyTestFakeAppName, "/", ""), nil)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusBadGateway, resp.StatusCode)
})
}