Skip to content

Commit 48a0beb

Browse files
committed
Fix terminals on the frontend to use proxies
1 parent 89efc57 commit 48a0beb

File tree

6 files changed

+107
-13
lines changed

6 files changed

+107
-13
lines changed

coderd/workspaceapps/proxy.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,10 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
616616

617617
conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
618618
CompressionMode: websocket.CompressionDisabled,
619+
OriginPatterns: []string{
620+
s.DashboardURL.Host,
621+
s.AccessURL.Host,
622+
},
619623
})
620624
if err != nil {
621625
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{

site/site.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,8 @@ func cspHeaders(next http.Handler) http.Handler {
319319
cspSrcs := CSPDirectives{
320320
// All omitted fetch csp srcs default to this.
321321
CSPDirectiveDefaultSrc: {"'self'"},
322-
CSPDirectiveConnectSrc: {"'self'"},
322+
// TODO: @emyrk fix this to only include the primary and proxy domains.
323+
CSPDirectiveConnectSrc: {"'self' ws: wss:"},
323324
CSPDirectiveChildSrc: {"'self'"},
324325
// https://github.com/suren-atoyan/monaco-react/issues/168
325326
CSPDirectiveScriptSrc: {"'self'"},

site/src/api/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,3 +1254,13 @@ export const watchBuildLogsByBuildId = (
12541254
})
12551255
return socket
12561256
}
1257+
1258+
export const issueReconnectingPTYSignedToken = async (
1259+
params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
1260+
): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
1261+
const response = await axios.post(
1262+
"/api/v2/applications/reconnecting-pty-signed-token",
1263+
params,
1264+
)
1265+
return response.data
1266+
}

site/src/components/TerminalLink/TerminalLink.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export const TerminalLink: FC<React.PropsWithChildren<TerminalLinkProps>> = ({
2828
userName = "me",
2929
workspaceName,
3030
}) => {
31-
32-
// Always use the primary for the terminal link. This is a relative link.
33-
const href = `/@${userName}/${workspaceName}${agentName ? `.${agentName}` : ""
34-
}/terminal`
31+
// Always use the primary for the terminal link. This is a relative link.
32+
const href = `/@${userName}/${workspaceName}${
33+
agentName ? `.${agentName}` : ""
34+
}/terminal`
3535

3636
return (
3737
<Link

site/src/pages/TerminalPage/TerminalPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const TerminalPage: FC<
7878
workspaceName: workspaceNameParts?.[0],
7979
username: username,
8080
command: command,
81+
baseURL: proxy.preferredPathAppURL,
8182
},
8283
actions: {
8384
readMessage: (_, event) => {

site/src/xServices/terminal/terminalXService.ts

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface TerminalContext {
1010
workspaceAgentError?: Error | unknown
1111
websocket?: WebSocket
1212
websocketError?: Error | unknown
13+
websocketURL?: string
14+
websocketURLError?: Error | unknown
1315

1416
// Assigned by connecting!
1517
// The workspace agent is entirely optional. If the agent is omitted the
@@ -19,6 +21,8 @@ export interface TerminalContext {
1921
workspaceName?: string
2022
reconnection?: string
2123
command?: string
24+
// If baseURL is not.....
25+
baseURL?: string
2226
}
2327

2428
export type TerminalEvent =
@@ -34,7 +38,7 @@ export type TerminalEvent =
3438
| { type: "DISCONNECT" }
3539

3640
export const terminalMachine =
37-
/** @xstate-layout N4IgpgJg5mDOIC5QBcwCcC2BLAdgQwBsBlZPVAOhmWVygHk0o8csAvMrAex1gGIJuYcrgBunANZCqDJi3Y1u8JCAAOnWFgU5EoAB6IA7AE4AzOSMBGAEwBWCyYAsANgs2bVgDQgAnohNGbcgsnEIcHCwAOBwAGCKMHAF8Er1RMXEISMikwaloZZjYORV50NE40chUCMgAzcoxKHPy5Ip4dVXVNLm1lfQQIiLMIpxMbQYjYo2jXL18EawtyAwGwk1HTMdGklPRsfGJSCioaHCgAdXLxWBU8AGMwfkFhHDFJRuQLtCub+-a1DS07T69icVnILisDhspiccQ2sz8y3INmiqNGsMcBmiNm2IFSewyh2yuVOn2+dwepXKlWqyDqmHeZOuFL+nUBvUQILBEKhMLhowRCAMTkCIzWDkcEyMBhsBlx+PSByy7xO50uzPuAEEYDhkI8cEJRBJiUyfmBtWBdayAd0gZyjCFyFZbKjnC4jKYLIK7GYYqirCYBk5olYDIlknjdorMkccqrTRSLbqSmgyhUqrV6oz1Wak8hrV1uHb5g6nE6XdE3RYPSYvT5EWCA2s7E4sbZhfKo-sY0JbtwDbdVfrDS9jeQ+zgB-nlP9Cz09IhIcLyCZIUYotEDCZNxEDN7peY1ms4gYrNNBp20t2ieP+2BB7QU2maZmGROpwX2QuEEuy6uHOuMRbjue71ggdiLPY4phiYMoWNYl4EkqFDvveqAQLwZwAEoAJIACoAKKfraHI-gYkTkCGAFYmG0TbnWcw2A4YKmGskJno44RWIh0Y3qhg6QLwWEEZqAAixFFqRobViusKngYMpWEGgpQmWUFsSEcSOHRPHXsq-Hobwok4UQADCdAAHIWQRpl4RJ84gH0SmtuQDgTNWMJTDKJgqUEq4RNCljTOs3ERgqekUBAWCwAZgnmVZNl2TObIkd+zplrEYanvY0SwrCESCs6BiuaidGrgGTgekYSQRjgnAQHA7ThYSyrHHkjAFPI3RKKAs5fo5iAxIEXHMSMErWNiTiFa4K6ldi1ayu4FjhjsV4tbGJJql8GpgPZxYWNMDiubBdh2Ju7pGIK-jFW6rYiq4bZOLp63EvGOaJjq069Slknfq4jrOii51udiMpXbC5ATKi1ghNEljNs9yG9neD6nHtUlnhErmgqeIyosKazejNZ7+qMi3BiYiM9rek5oZA6NpeR0ROmGpgnhT0qCmNSxHhKW4BiGERUzeUUxSj6EMwN4HM5ukLLXBViRKGNhXVj64etM0JOG5YTC1kktORlp7hA4CtK2DYEALQQ8M5FKQd27OJTNVAA */
41+
/** @xstate-layout N4IgpgJg5mDOIC5QBcwCcC2BLAdgQwBsBlZPVAOljGQFcAHAYggHscxLSLVNdCSz2VWnQDaABgC6iUHWawsyLK2kgAHogAcAVgCM5MQGYdAJgMaALOYDsV8zq0AaEAE9ExgGwHyATmPf3VmI6Yub+5gYAvhFO3Nj4xJyC1PTkMMgA6sxoANawdHgAxuxpijhQmTl5hWBMrOy4AG7M2cXUFbn5ReJSSCCy8orKveoI5qbkOhq23hozflruxuZOrghGXuZaRsFiWlbeWuYa7lEx6HF8iZTJdKltWR3Vd8il5Q9VRQzoaFnkdARkABmWQwz3aHzA3RU-QUShwKhGYy8k2msw080WyxcbmMYnIoW8hO8ZkMYisOlOIFivASAmer3BnTAAEEYDhkLU2ORGs1Whl3kzWWB2VDejDBvDhogdAYrFoJuYxJjdmJvCENCs3Fo8e4glpjFptWSDhpKdT4vwKCVcG9KoK2Rzvr9-kCQWCBdUhSLJNC5LChqARotNQgpniNAYtAdjFYDL4pidolTzjTLXyGWAAEZEZgFFrIACqACUADKc+o4JotZ45vPUYsl0UyP0ShGIWVWchGA27Akzbwh4LjDQmAk6AmBbxmlMWq7WsrpLO1-MNr5oH5oP4A5DAzA13Mr0tNvotuFthBWYyDo56ULGewWHTk0zGac8Wd0gqsNgFV7l7mVry5BfjgP7IMe4pnlKobmO45D3mG7ihFMBgBIOhgaPoizar4xIRv4b4XLSFAgWBNprhuW6unupFgL+EGngGaiIMG2IIPYtj4psMYaGIxh+Do9iEamVy0b+kAMOkRYAJIACoAKIMQMUGBogdiYZs3Z+N444GqYg42F4kY6LqmxWLMUaREm5qXJ+350agEAMEW8nMgAIkp-qSqpow6N45BIRYkYRgsBxyoOASdnG2gyu43jcUhwkfiR9niU5bnSUQADCADyAByeXyVlsmea20F+Zho6EksgU6WIGpsZMuj6OZfimLB0bmEltkUBAWCwGJjkMLlBVFSVPpiox3nMexV6Na+lI4MwEBwCoNnEWAvrKUxIwALRjCGu1yuQRpiCEBpyrshLdRt1zCFtXnnu4IabCdZ1nWMezalGFLWTOPVJMI7p2tUD1lT5hyDgYXjvR9KrxSZXV-e+AN3SkaSMk8862o8RRgypM1DiGL4+FY7iGvqniXvYSNnCjt1COj9wg0UlA0AURSwPAk3bdNQbQ-o3YLCYHGwcTRwBeE-GYjToRWDdab0jamNFF6yD4ztiD+PKgTdtYljuAEBjE3G+J+HFI7ah45IK3O1AZtmB71qWGt84gezofe+j2Lq7h+XxSFWXTRGK4NNqu09fFYUssyLPxCyOI1hjyh4CzW1b3jy8jIeialjkR9BhzweTiwBPqOm2Asg5TOYXbqmY6xISEtt0n1A155ABc+aheiGCYfhGAnthWIO33kOSxwRn3vgBFEURAA */
3842
createMachine(
3943
{
4044
id: "terminalState",
@@ -50,6 +54,9 @@ export const terminalMachine =
5054
getWorkspaceAgent: {
5155
data: TypesGen.WorkspaceAgent
5256
}
57+
getWebsocketURL: {
58+
data: string
59+
}
5360
connect: {
5461
data: WebSocket
5562
}
@@ -98,7 +105,7 @@ export const terminalMachine =
98105
onDone: [
99106
{
100107
actions: ["assignWorkspaceAgent", "clearWorkspaceAgentError"],
101-
target: "connecting",
108+
target: "gettingWebSocketURL",
102109
},
103110
],
104111
onError: [
@@ -109,6 +116,24 @@ export const terminalMachine =
109116
],
110117
},
111118
},
119+
gettingWebSocketURL: {
120+
invoke: {
121+
src: "getWebsocketURL",
122+
id: "getWebsocketURL",
123+
onDone: [
124+
{
125+
actions: ["assignWebsocketURL", "clearWebsocketURLError"],
126+
target: "connecting",
127+
},
128+
],
129+
onError: [
130+
{
131+
actions: "assignWebsocketURLError",
132+
target: "disconnected",
133+
},
134+
],
135+
},
136+
},
112137
connecting: {
113138
invoke: {
114139
src: "connect",
@@ -185,17 +210,60 @@ export const terminalMachine =
185210
}
186211
return agent
187212
},
213+
getWebsocketURL: async (context) => {
214+
if (!context.workspaceAgent) {
215+
throw new Error("workspace agent is not set")
216+
}
217+
if (!context.reconnection) {
218+
throw new Error("reconnection ID is not set")
219+
}
220+
221+
let baseURL = context.baseURL || ""
222+
if (!baseURL) {
223+
baseURL = `${location.protocol}//${location.host}`
224+
}
225+
226+
const query = new URLSearchParams({
227+
reconnect: context.reconnection,
228+
})
229+
if (context.command) {
230+
query.set("command", context.command)
231+
}
232+
233+
const url = new URL(baseURL)
234+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
235+
if (!url.pathname.endsWith("/")) {
236+
url.pathname + "/"
237+
}
238+
url.pathname += `api/v2/workspaceagents/${context.workspaceAgent.id}/pty`
239+
url.search = "?" + query.toString()
240+
241+
// If the URL is just the primary API, we don't need a signed token to
242+
// connect.
243+
if (!context.baseURL) {
244+
return url.toString()
245+
}
246+
247+
// Do ticket issuance and set the query parameter.
248+
const tokenRes = await API.issueReconnectingPTYSignedToken({
249+
url: url.toString(),
250+
agentID: context.workspaceAgent.id,
251+
})
252+
query.set("coder_signed_app_token_23db1dde", tokenRes.signed_token)
253+
url.search = "?" + query.toString()
254+
255+
return url.toString()
256+
},
188257
connect: (context) => (send) => {
189258
return new Promise<WebSocket>((resolve, reject) => {
190259
if (!context.workspaceAgent) {
191260
return reject("workspace agent is not set")
192261
}
193-
const proto = location.protocol === "https:" ? "wss:" : "ws:"
194-
const commandQuery = context.command
195-
? `&command=${encodeURIComponent(context.command)}`
196-
: ""
197-
const url = `${proto}//${location.host}/api/v2/workspaceagents/${context.workspaceAgent.id}/pty?reconnect=${context.reconnection}${commandQuery}`
198-
const socket = new WebSocket(url)
262+
if (!context.websocketURL) {
263+
return reject("websocket URL is not set")
264+
}
265+
266+
const socket = new WebSocket(context.websocketURL)
199267
socket.binaryType = "arraybuffer"
200268
socket.addEventListener("open", () => {
201269
resolve(socket)
@@ -254,6 +322,16 @@ export const terminalMachine =
254322
...context,
255323
webSocketError: undefined,
256324
})),
325+
assignWebsocketURL: assign({
326+
websocketURL: (context, event) => event.data ?? context.websocketURL,
327+
}),
328+
assignWebsocketURLError: assign({
329+
websocketURLError: (_, event) => event.data,
330+
}),
331+
clearWebsocketURLError: assign((context: TerminalContext) => ({
332+
...context,
333+
websocketURLError: undefined,
334+
})),
257335
sendMessage: (context, event) => {
258336
if (!context.websocket) {
259337
throw new Error("websocket doesn't exist")

0 commit comments

Comments
 (0)