Skip to content

feat: support localhost apps running https #8585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions coderd/tailnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package coderd
import (
"bufio"
"context"
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
Expand Down Expand Up @@ -70,6 +71,17 @@ func NewServerTailnet(
tn.transport.DialContext = tn.dialContext
tn.transport.MaxIdleConnsPerHost = 10
tn.transport.MaxIdleConns = 0
// We intentionally don't verify the certificate chain here.
// The connection to the workspace is already established and most
// apps are already going to be accessed over plain HTTP, this config
// simply allows apps being run over HTTPS to be accessed without error --
// many of which may be using self-signed certs.
tn.transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
//nolint:gosec
InsecureSkipVerify: true,
}
Comment on lines +79 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also add NextProtos: []string{"h2", "http/1.1"}, to allow http2.


agentConn, err := getMultiAgent(ctx)
if err != nil {
return nil, xerrors.Errorf("get initial multi agent: %w", err)
Expand Down
125 changes: 82 additions & 43 deletions coderd/tailnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,66 +62,105 @@ func TestServerTailnet_AgentConn_Legacy(t *testing.T) {
assert.True(t, conn.AwaitReachable(ctx))
}

func TestServerTailnet_ReverseProxy_OK(t *testing.T) {
func TestServerTailnet_ReverseProxy(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
t.Run("OK", func(t *testing.T) {
t.Parallel()

// Force a connection through wsconncache using the legacy hardcoded ip.
agentID, _, serverTailnet := setupAgent(t, nil)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", codersdk.WorkspaceAgentHTTPAPIServerPort))
require.NoError(t, err)
agentID, _, serverTailnet := setupAgent(t, nil)

rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
require.NoError(t, err)
defer release()
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", codersdk.WorkspaceAgentHTTPAPIServerPort))
require.NoError(t, err)

rw := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
u.String(),
nil,
).WithContext(ctx)
rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
require.NoError(t, err)
defer release()

rp.ServeHTTP(rw, req)
res := rw.Result()
defer res.Body.Close()
rw := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
u.String(),
nil,
).WithContext(ctx)

assert.Equal(t, http.StatusOK, res.StatusCode)
}
rp.ServeHTTP(rw, req)
res := rw.Result()
defer res.Body.Close()

func TestServerTailnet_ReverseProxy_Legacy(t *testing.T) {
t.Parallel()
assert.Equal(t, http.StatusOK, res.StatusCode)
})

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
t.Run("HTTPSProxy", func(t *testing.T) {
t.Parallel()

// Force a connection through wsconncache using the legacy hardcoded ip.
agentID, _, serverTailnet := setupAgent(t, []netip.Prefix{
netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128),
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

agentID, _, serverTailnet := setupAgent(t, nil)

const expectedResponseCode = 209
// Test that we can proxy HTTPS traffic.
s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(expectedResponseCode)
}))
t.Cleanup(s.Close)

uri, err := url.Parse(s.URL)
require.NoError(t, err)

rp, release, err := serverTailnet.ReverseProxy(uri, uri, agentID)
require.NoError(t, err)
defer release()

rw := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
uri.String(),
nil,
).WithContext(ctx)

rp.ServeHTTP(rw, req)
res := rw.Result()
defer res.Body.Close()

assert.Equal(t, expectedResponseCode, res.StatusCode)
})

u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", codersdk.WorkspaceAgentHTTPAPIServerPort))
require.NoError(t, err)
t.Run("Legacy", func(t *testing.T) {
t.Parallel()

rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
require.NoError(t, err)
defer release()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

// Force a connection through wsconncache using the legacy hardcoded ip.
agentID, _, serverTailnet := setupAgent(t, []netip.Prefix{
netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128),
})

u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", codersdk.WorkspaceAgentHTTPAPIServerPort))
require.NoError(t, err)

rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
require.NoError(t, err)
defer release()

rw := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
u.String(),
nil,
).WithContext(ctx)
rw := httptest.NewRecorder()
req := httptest.NewRequest(
http.MethodGet,
u.String(),
nil,
).WithContext(ctx)

rp.ServeHTTP(rw, req)
res := rw.Result()
defer res.Body.Close()
rp.ServeHTTP(rw, req)
res := rw.Result()
defer res.Body.Close()

assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, http.StatusOK, res.StatusCode)
})
}

func setupAgent(t *testing.T, agentAddresses []netip.Prefix) (uuid.UUID, agent.Agent, *coderd.ServerTailnet) {
Expand Down
93 changes: 91 additions & 2 deletions coderd/workspaceapps/apptest/apptest.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,51 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, http.StatusOK, resp.StatusCode)
})

t.Run("ProxiesHTTPS", func(t *testing.T) {
t.Parallel()

appDetails := setupProxyTest(t, &DeploymentOptions{
ServeHTTPS: true,
})

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

u := appDetails.PathAppURL(appDetails.Apps.Owner)
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)

var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.DevURLSignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")

// Ensure the signed app token cookie is valid.
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)
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("BlocksMe", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -762,6 +807,50 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, http.StatusOK, resp.StatusCode)
})

t.Run("ProxiesHTTPS", func(t *testing.T) {
t.Parallel()

appDetails := setupProxyTest(t, &DeploymentOptions{
ServeHTTPS: true,
})
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
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)

var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.DevURLSignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed token cookie in response")
require.Equal(t, appTokenCookie.Path, "/", "incorrect path on signed token cookie")

// Ensure the signed app token cookie is valid.
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)
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()

Expand Down Expand Up @@ -928,8 +1017,8 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
forceURLTransport(t, client)

// Create workspace.
port := appServer(t, nil)
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port)
port := appServer(t, nil, false)
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)

// Verify that the apps have the correct sharing levels set.
workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
Expand Down
Loading