Skip to content

Commit 866eeed

Browse files
committed
Add proxying based on path
1 parent 934b1ff commit 866eeed

File tree

7 files changed

+158
-92
lines changed

7 files changed

+158
-92
lines changed

coderd/coderd.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package coderd
22

33
import (
44
"context"
5-
"crypto/cipher"
65
"crypto/x509"
76
"fmt"
87
"net/http"
@@ -36,11 +35,10 @@ import (
3635

3736
// Options are requires parameters for Coder to start.
3837
type Options struct {
39-
AccessURL *url.URL
40-
WildcardURL *url.URL
41-
Logger slog.Logger
42-
Database database.Store
43-
Pubsub database.Pubsub
38+
AccessURL *url.URL
39+
Logger slog.Logger
40+
Database database.Store
41+
Pubsub database.Pubsub
4442

4543
AgentConnectionUpdateFrequency time.Duration
4644
// APIRateLimit is the minutely throughput rate limit per user or ip.
@@ -57,9 +55,6 @@ type Options struct {
5755
SSHKeygenAlgorithm gitsshkey.Algorithm
5856
TURNServer *turnconn.Server
5957
TracerProvider *sdktrace.TracerProvider
60-
// WildcardCipher is used to encrypt session tokens so that authentication
61-
// can be securely transferred to the wildcard host.
62-
WildcardCipher cipher.AEAD
6358
}
6459

6560
// New constructs a Coder API handler.
@@ -109,6 +104,7 @@ func New(options *Options) *API {
109104
httpmw.ExtractUserParam(api.Database),
110105
authRolesMiddleware,
111106
)
107+
r.Get("/", api.workspaceAppsProxyPath)
112108
})
113109

114110
r.Route("/api/v2", func(r chi.Router) {

coderd/httpmw/wildcard.go

Lines changed: 0 additions & 24 deletions
This file was deleted.

coderd/httpmw/wildcard_test.go

Lines changed: 0 additions & 38 deletions
This file was deleted.

coderd/workspaceagents.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,11 +462,10 @@ func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
462462
apps := make([]codersdk.WorkspaceApp, 0)
463463
for _, dbApp := range dbApps {
464464
apps = append(apps, codersdk.WorkspaceApp{
465-
ID: dbApp.ID,
466-
Name: dbApp.Name,
467-
Command: dbApp.Command.String,
468-
AccessURL: dbApp.Url.String,
469-
Icon: dbApp.Icon,
465+
ID: dbApp.ID,
466+
Name: dbApp.Name,
467+
Command: dbApp.Command.String,
468+
Icon: dbApp.Icon,
470469
})
471470
}
472471
return apps

coderd/workspaceapps.go

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,133 @@
11
package coderd
22

33
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
47
"net/http"
8+
"net/http/httputil"
9+
"net/url"
10+
"strings"
511

6-
"github.com/coder/coder/coderd/database"
12+
"github.com/go-chi/chi/v5"
713
"github.com/google/uuid"
8-
)
914

10-
// workspaceAppsAuthWildcard authenticates the wildcard domain.
11-
func (api *API) workspaceAppsAuthWildcard(rw http.ResponseWriter, r *http.Request) {
12-
// r.URL.Query().Get("redirect")
15+
"github.com/coder/coder/coderd/database"
16+
"github.com/coder/coder/coderd/httpapi"
17+
"github.com/coder/coder/coderd/httpmw"
18+
)
1319

14-
}
20+
func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
21+
user := httpmw.UserParam(r)
22+
// This can be in the form of: "<workspace-name>.[workspace-agent]" or "<workspace-name>"
23+
workspaceWithAgent := chi.URLParam(r, "workspaceagent")
24+
workspaceParts := strings.Split(workspaceWithAgent, ".")
1525

16-
func (api *API) workspaceAppsProxyWildcard(rw http.ResponseWriter, r *http.Request) {
26+
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
27+
OwnerID: user.ID,
28+
Name: workspaceParts[0],
29+
})
30+
if errors.Is(err, sql.ErrNoRows) {
31+
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
32+
Message: "workspace not found",
33+
})
34+
return
35+
}
36+
if err != nil {
37+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
38+
Message: fmt.Sprintf("get workspace: %s", err),
39+
})
40+
return
41+
}
1742

18-
}
43+
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
44+
if err != nil {
45+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
46+
Message: fmt.Sprintf("get workspace build: %s", err),
47+
})
48+
return
49+
}
1950

20-
func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
21-
conn, err := api.dialWorkspaceAgent(r, uuid.Nil)
51+
resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), build.JobID)
2252
if err != nil {
53+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
54+
Message: fmt.Sprintf("get workspace resources: %s", err),
55+
})
2356
return
2457
}
58+
resourceIDs := make([]uuid.UUID, 0)
59+
for _, resource := range resources {
60+
resourceIDs = append(resourceIDs, resource.ID)
61+
}
62+
agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs)
63+
if err != nil {
64+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
65+
Message: fmt.Sprintf("get workspace agents: %s", err),
66+
})
67+
return
68+
}
69+
if len(agents) == 0 {
70+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
71+
Message: "no agents exist",
72+
})
73+
}
74+
75+
agent := agents[0]
76+
if len(workspaceParts) > 1 {
77+
for _, otherAgent := range agents {
78+
if otherAgent.Name == workspaceParts[1] {
79+
agent = otherAgent
80+
break
81+
}
82+
}
83+
}
84+
2585
app, err := api.Database.GetWorkspaceAppByAgentIDAndName(r.Context(), database.GetWorkspaceAppByAgentIDAndNameParams{
26-
AgentID: uuid.Nil,
27-
Name: "something",
86+
AgentID: agent.ID,
87+
Name: chi.URLParam(r, "application"),
2888
})
89+
if errors.Is(err, sql.ErrNoRows) {
90+
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
91+
Message: "application not found",
92+
})
93+
return
94+
}
2995
if err != nil {
96+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
97+
Message: fmt.Sprintf("get workspace app: %s", err),
98+
})
3099
return
31100
}
32-
conn.DialContext(r.Context(), "tcp", "localhost:3000")
101+
if !app.Url.Valid {
102+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
103+
Message: fmt.Sprintf("application does not have a url: %s", err),
104+
})
105+
return
106+
}
107+
108+
appURL, err := url.Parse(app.Url.String)
109+
if err != nil {
110+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
111+
Message: fmt.Sprintf("parse app url: %s", err),
112+
})
113+
return
114+
}
115+
116+
conn, err := api.dialWorkspaceAgent(r, agent.ID)
117+
if err != nil {
118+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
119+
Message: fmt.Sprintf("dial workspace agent: %s", err),
120+
})
121+
return
122+
}
123+
124+
proxy := httputil.NewSingleHostReverseProxy(appURL)
125+
defaultTransport, valid := http.DefaultTransport.(*http.Transport)
126+
if !valid {
127+
panic("dev error: default transport isn't a transport")
128+
}
129+
transport := defaultTransport.Clone()
130+
transport.DialContext = conn.DialContext
131+
proxy.Transport = transport
132+
proxy.ServeHTTP(rw, r)
33133
}

coderd/workspaceapps_test.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
11
package coderd_test
22

33
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net"
8+
"net/http"
49
"testing"
510

11+
"github.com/google/uuid"
12+
"github.com/stretchr/testify/require"
13+
614
"cdr.dev/slog/sloggers/slogtest"
715
"github.com/coder/coder/agent"
816
"github.com/coder/coder/coderd/coderdtest"
917
"github.com/coder/coder/codersdk"
1018
"github.com/coder/coder/provisioner/echo"
1119
"github.com/coder/coder/provisionersdk/proto"
12-
"github.com/google/uuid"
1320
)
1421

1522
func TestWorkspaceAppsProxyPath(t *testing.T) {
1623
t.Parallel()
1724
t.Run("Proxies", func(t *testing.T) {
1825
t.Parallel()
26+
// #nosec
27+
ln, err := net.Listen("tcp", ":0")
28+
require.NoError(t, err)
29+
server := http.Server{
30+
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31+
w.WriteHeader(http.StatusOK)
32+
}),
33+
}
34+
t.Cleanup(func() {
35+
_ = server.Close()
36+
_ = ln.Close()
37+
})
38+
go server.Serve(ln)
39+
tcpAddr, _ := ln.Addr().(*net.TCPAddr)
40+
1941
client, coderAPI := coderdtest.NewWithAPI(t, nil)
2042
user := coderdtest.CreateFirstUser(t, client)
2143
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
@@ -34,6 +56,10 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
3456
Auth: &proto.Agent_Token{
3557
Token: authToken,
3658
},
59+
Apps: []*proto.App{{
60+
Name: "example",
61+
Url: fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port),
62+
}},
3763
}},
3864
}},
3965
},
@@ -54,6 +80,13 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
5480
t.Cleanup(func() {
5581
_ = agentCloser.Close()
5682
})
57-
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
83+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
84+
85+
resp, err := client.Request(context.Background(), http.MethodGet, "/me/"+workspace.Name+"/example", nil)
86+
require.NoError(t, err)
87+
body, err := io.ReadAll(resp.Body)
88+
require.NoError(t, err)
89+
require.Equal(t, "", string(body))
90+
require.Equal(t, http.StatusOK, resp.StatusCode)
5891
})
5992
}

coderd/workspaceresources_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestWorkspaceResource(t *testing.T) {
4646

4747
t.Run("Apps", func(t *testing.T) {
4848
t.Parallel()
49-
_, client, coderd := coderdtest.NewWithServer(t, nil)
49+
client, coderd := coderdtest.NewWithAPI(t, nil)
5050
user := coderdtest.CreateFirstUser(t, client)
5151
coderdtest.NewProvisionerDaemon(t, coderd)
5252
app := &proto.App{

0 commit comments

Comments
 (0)