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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 82 additions & 43 deletions
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

Lines changed: 91 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy