Skip to content

Commit 80b940c

Browse files
authored
feat: support localhost apps running https (#8585)
1 parent 00b9a3c commit 80b940c

File tree

5 files changed

+241
-92
lines changed

5 files changed

+241
-92
lines changed

coderd/tailnet.go

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package coderd
33
import (
44
"bufio"
55
"context"
6+
"crypto/tls"
67
"net"
78
"net/http"
89
"net/http/httputil"
@@ -70,6 +71,17 @@ func NewServerTailnet(
7071
tn.transport.DialContext = tn.dialContext
7172
tn.transport.MaxIdleConnsPerHost = 10
7273
tn.transport.MaxIdleConns = 0
74+
// We intentionally don't verify the certificate chain here.
75+
// The connection to the workspace is already established and most
76+
// apps are already going to be accessed over plain HTTP, this config
77+
// simply allows apps being run over HTTPS to be accessed without error --
78+
// many of which may be using self-signed certs.
79+
tn.transport.TLSClientConfig = &tls.Config{
80+
MinVersion: tls.VersionTLS12,
81+
//nolint:gosec
82+
InsecureSkipVerify: true,
83+
}
84+
7385
agentConn, err := getMultiAgent(ctx)
7486
if err != nil {
7587
return nil, xerrors.Errorf("get initial multi agent: %w", err)

coderd/tailnet_test.go

+82-43
Original file line numberDiff line numberDiff line change
@@ -62,66 +62,105 @@ func TestServerTailnet_AgentConn_Legacy(t *testing.T) {
6262
assert.True(t, conn.AwaitReachable(ctx))
6363
}
6464

65-
func TestServerTailnet_ReverseProxy_OK(t *testing.T) {
65+
func TestServerTailnet_ReverseProxy(t *testing.T) {
6666
t.Parallel()
6767

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

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

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

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

81-
rw := httptest.NewRecorder()
82-
req := httptest.NewRequest(
83-
http.MethodGet,
84-
u.String(),
85-
nil,
86-
).WithContext(ctx)
79+
rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
80+
require.NoError(t, err)
81+
defer release()
8782

88-
rp.ServeHTTP(rw, req)
89-
res := rw.Result()
90-
defer res.Body.Close()
83+
rw := httptest.NewRecorder()
84+
req := httptest.NewRequest(
85+
http.MethodGet,
86+
u.String(),
87+
nil,
88+
).WithContext(ctx)
9189

92-
assert.Equal(t, http.StatusOK, res.StatusCode)
93-
}
90+
rp.ServeHTTP(rw, req)
91+
res := rw.Result()
92+
defer res.Body.Close()
9493

95-
func TestServerTailnet_ReverseProxy_Legacy(t *testing.T) {
96-
t.Parallel()
94+
assert.Equal(t, http.StatusOK, res.StatusCode)
95+
})
9796

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

101-
// Force a connection through wsconncache using the legacy hardcoded ip.
102-
agentID, _, serverTailnet := setupAgent(t, []netip.Prefix{
103-
netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128),
100+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
101+
defer cancel()
102+
103+
agentID, _, serverTailnet := setupAgent(t, nil)
104+
105+
const expectedResponseCode = 209
106+
// Test that we can proxy HTTPS traffic.
107+
s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
108+
w.WriteHeader(expectedResponseCode)
109+
}))
110+
t.Cleanup(s.Close)
111+
112+
uri, err := url.Parse(s.URL)
113+
require.NoError(t, err)
114+
115+
rp, release, err := serverTailnet.ReverseProxy(uri, uri, agentID)
116+
require.NoError(t, err)
117+
defer release()
118+
119+
rw := httptest.NewRecorder()
120+
req := httptest.NewRequest(
121+
http.MethodGet,
122+
uri.String(),
123+
nil,
124+
).WithContext(ctx)
125+
126+
rp.ServeHTTP(rw, req)
127+
res := rw.Result()
128+
defer res.Body.Close()
129+
130+
assert.Equal(t, expectedResponseCode, res.StatusCode)
104131
})
105132

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

109-
rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
110-
require.NoError(t, err)
111-
defer release()
136+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
137+
defer cancel()
138+
139+
// Force a connection through wsconncache using the legacy hardcoded ip.
140+
agentID, _, serverTailnet := setupAgent(t, []netip.Prefix{
141+
netip.PrefixFrom(codersdk.WorkspaceAgentIP, 128),
142+
})
143+
144+
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", codersdk.WorkspaceAgentHTTPAPIServerPort))
145+
require.NoError(t, err)
146+
147+
rp, release, err := serverTailnet.ReverseProxy(u, u, agentID)
148+
require.NoError(t, err)
149+
defer release()
112150

113-
rw := httptest.NewRecorder()
114-
req := httptest.NewRequest(
115-
http.MethodGet,
116-
u.String(),
117-
nil,
118-
).WithContext(ctx)
151+
rw := httptest.NewRecorder()
152+
req := httptest.NewRequest(
153+
http.MethodGet,
154+
u.String(),
155+
nil,
156+
).WithContext(ctx)
119157

120-
rp.ServeHTTP(rw, req)
121-
res := rw.Result()
122-
defer res.Body.Close()
158+
rp.ServeHTTP(rw, req)
159+
res := rw.Result()
160+
defer res.Body.Close()
123161

124-
assert.Equal(t, http.StatusOK, res.StatusCode)
162+
assert.Equal(t, http.StatusOK, res.StatusCode)
163+
})
125164
}
126165

127166
func setupAgent(t *testing.T, agentAddresses []netip.Prefix) (uuid.UUID, agent.Agent, *coderd.ServerTailnet) {

coderd/workspaceapps/apptest/apptest.go

+91-2
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,51 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
349349
require.Equal(t, http.StatusOK, resp.StatusCode)
350350
})
351351

352+
t.Run("ProxiesHTTPS", func(t *testing.T) {
353+
t.Parallel()
354+
355+
appDetails := setupProxyTest(t, &DeploymentOptions{
356+
ServeHTTPS: true,
357+
})
358+
359+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
360+
defer cancel()
361+
362+
u := appDetails.PathAppURL(appDetails.Apps.Owner)
363+
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
364+
require.NoError(t, err)
365+
defer resp.Body.Close()
366+
body, err := io.ReadAll(resp.Body)
367+
require.NoError(t, err)
368+
require.Equal(t, proxyTestAppBody, string(body))
369+
require.Equal(t, http.StatusOK, resp.StatusCode)
370+
371+
var appTokenCookie *http.Cookie
372+
for _, c := range resp.Cookies() {
373+
if c.Name == codersdk.DevURLSignedAppTokenCookie {
374+
appTokenCookie = c
375+
break
376+
}
377+
}
378+
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
379+
require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")
380+
381+
// Ensure the signed app token cookie is valid.
382+
appTokenClient := appDetails.AppClient(t)
383+
appTokenClient.SetSessionToken("")
384+
appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
385+
require.NoError(t, err)
386+
appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})
387+
388+
resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
389+
require.NoError(t, err)
390+
defer resp.Body.Close()
391+
body, err = io.ReadAll(resp.Body)
392+
require.NoError(t, err)
393+
require.Equal(t, proxyTestAppBody, string(body))
394+
require.Equal(t, http.StatusOK, resp.StatusCode)
395+
})
396+
352397
t.Run("BlocksMe", func(t *testing.T) {
353398
t.Parallel()
354399

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

810+
t.Run("ProxiesHTTPS", func(t *testing.T) {
811+
t.Parallel()
812+
813+
appDetails := setupProxyTest(t, &DeploymentOptions{
814+
ServeHTTPS: true,
815+
})
816+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
817+
defer cancel()
818+
819+
u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
820+
resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
821+
require.NoError(t, err)
822+
defer resp.Body.Close()
823+
body, err := io.ReadAll(resp.Body)
824+
require.NoError(t, err)
825+
require.Equal(t, proxyTestAppBody, string(body))
826+
require.Equal(t, http.StatusOK, resp.StatusCode)
827+
828+
var appTokenCookie *http.Cookie
829+
for _, c := range resp.Cookies() {
830+
if c.Name == codersdk.DevURLSignedAppTokenCookie {
831+
appTokenCookie = c
832+
break
833+
}
834+
}
835+
require.NotNil(t, appTokenCookie, "no signed token cookie in response")
836+
require.Equal(t, appTokenCookie.Path, "/", "incorrect path on signed token cookie")
837+
838+
// Ensure the signed app token cookie is valid.
839+
appTokenClient := appDetails.AppClient(t)
840+
appTokenClient.SetSessionToken("")
841+
appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
842+
require.NoError(t, err)
843+
appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})
844+
845+
resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
846+
require.NoError(t, err)
847+
defer resp.Body.Close()
848+
body, err = io.ReadAll(resp.Body)
849+
require.NoError(t, err)
850+
require.Equal(t, proxyTestAppBody, string(body))
851+
require.Equal(t, http.StatusOK, resp.StatusCode)
852+
})
853+
765854
t.Run("ProxiesPort", func(t *testing.T) {
766855
t.Parallel()
767856

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

9301019
// Create workspace.
931-
port := appServer(t, nil)
932-
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port)
1020+
port := appServer(t, nil, false)
1021+
workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)
9331022

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

0 commit comments

Comments
 (0)