mirror of https://github.com/coder/coder.git
feat: support localhost apps running https (#8585)
This commit is contained in:
parent
00b9a3ce58
commit
80b940c556
|
@ -3,6 +3,7 @@ package coderd
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
agentConn, err := getMultiAgent(ctx)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get initial multi agent: %w", err)
|
||||
|
|
|
@ -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()
|
||||
|
||||
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),
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
|
||||
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", codersdk.WorkspaceAgentHTTPAPIServerPort))
|
||||
require.NoError(t, err)
|
||||
t.Run("HTTPSProxy", 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()
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(
|
||||
http.MethodGet,
|
||||
u.String(),
|
||||
nil,
|
||||
).WithContext(ctx)
|
||||
agentID, _, serverTailnet := setupAgent(t, nil)
|
||||
|
||||
rp.ServeHTTP(rw, req)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
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)
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("Legacy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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)
|
||||
|
||||
rp.ServeHTTP(rw, req)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func setupAgent(t *testing.T, agentAddresses []netip.Prefix) (uuid.UUID, agent.Agent, *coderd.ServerTailnet) {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
@ -48,6 +49,7 @@ type DeploymentOptions struct {
|
|||
DisableSubdomainApps bool
|
||||
DangerousAllowPathAppSharing bool
|
||||
DangerousAllowPathAppSiteOwnerAccess bool
|
||||
ServeHTTPS bool
|
||||
|
||||
// The following fields are only used by setupProxyTestWithFactory.
|
||||
noWorkspace bool
|
||||
|
@ -185,9 +187,9 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
|
|||
}
|
||||
|
||||
if opts.port == 0 {
|
||||
opts.port = appServer(t, opts.headers)
|
||||
opts.port = appServer(t, opts.headers, opts.ServeHTTPS)
|
||||
}
|
||||
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port)
|
||||
workspace, agnt := createWorkspaceWithApps(t, deployment.SDKClient, deployment.FirstUser.OrganizationID, me, opts.port, opts.ServeHTTPS)
|
||||
|
||||
details := &Details{
|
||||
Deployment: deployment,
|
||||
|
@ -234,60 +236,53 @@ func setupProxyTestWithFactory(t *testing.T, factory DeploymentFactory, opts *De
|
|||
return details
|
||||
}
|
||||
|
||||
func appServer(t *testing.T, headers http.Header) uint16 {
|
||||
// Start a listener on a random port greater than the minimum app port.
|
||||
var (
|
||||
ln net.Listener
|
||||
tcpAddr *net.TCPAddr
|
||||
)
|
||||
for i := 0; i < 32; i++ {
|
||||
var err error
|
||||
// #nosec
|
||||
ln, err = net.Listen("tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
var ok bool
|
||||
tcpAddr, ok = ln.Addr().(*net.TCPAddr)
|
||||
require.True(t, ok)
|
||||
if tcpAddr.Port < codersdk.WorkspaceAgentMinimumListeningPort {
|
||||
_ = ln.Close()
|
||||
ln = nil
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
require.NotNil(t, ln, "failed to find a free port greater than the minimum app port")
|
||||
|
||||
server := http.Server{
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||
assert.ErrorIs(t, err, http.ErrNoCookie)
|
||||
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
w.Header().Add(name, value)
|
||||
//nolint:revive
|
||||
func appServer(t *testing.T, headers http.Header, isHTTPS bool) uint16 {
|
||||
server := httptest.NewUnstartedServer(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||
assert.ErrorIs(t, err, http.ErrNoCookie)
|
||||
w.Header().Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For"))
|
||||
for name, values := range headers {
|
||||
for _, value := range values {
|
||||
w.Header().Add(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(proxyTestAppBody))
|
||||
}),
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(proxyTestAppBody))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
server.Config.ReadHeaderTimeout = time.Minute
|
||||
if isHTTPS {
|
||||
server.StartTLS()
|
||||
} else {
|
||||
server.Start()
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
_ = ln.Close()
|
||||
server.Close()
|
||||
})
|
||||
go func() {
|
||||
_ = server.Serve(ln)
|
||||
}()
|
||||
|
||||
return uint16(tcpAddr.Port)
|
||||
_, portStr, err := net.SplitHostPort(server.Listener.Addr().String())
|
||||
require.NoError(t, err)
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
require.NoError(t, err)
|
||||
|
||||
return uint16(port)
|
||||
}
|
||||
|
||||
func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.UUID, me codersdk.User, port uint16, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (codersdk.Workspace, codersdk.WorkspaceAgent) {
|
||||
//nolint:revive
|
||||
func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.UUID, me codersdk.User, port uint16, serveHTTPS bool, workspaceMutators ...func(*codersdk.CreateWorkspaceRequest)) (codersdk.Workspace, codersdk.WorkspaceAgent) {
|
||||
authToken := uuid.NewString()
|
||||
|
||||
appURL := fmt.Sprintf("http://127.0.0.1:%d?%s", port, proxyTestAppQuery)
|
||||
scheme := "http"
|
||||
if serveHTTPS {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
|
|
|
@ -4,6 +4,7 @@ package wsconncache
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
@ -49,8 +50,9 @@ func (a *AgentProvider) ReverseProxy(targetURL *url.URL, dashboardURL *url.URL,
|
|||
return nil, nil, xerrors.Errorf("acquire agent connection: %w", err)
|
||||
}
|
||||
|
||||
proxy.Transport = conn.HTTPTransport()
|
||||
transport := conn.HTTPTransport()
|
||||
|
||||
proxy.Transport = transport
|
||||
return proxy, release, nil
|
||||
}
|
||||
|
||||
|
@ -154,6 +156,18 @@ func (c *Cache) Acquire(id uuid.UUID) (*Conn, func(), error) {
|
|||
}
|
||||
transport := defaultTransport.Clone()
|
||||
transport.DialContext = agentConn.DialContext
|
||||
|
||||
// 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.
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
conn := &Conn{
|
||||
WorkspaceAgentConn: agentConn,
|
||||
timeoutCancel: timeoutCancelFunc,
|
||||
|
|
Loading…
Reference in New Issue