Skip to content

Commit 0e4749b

Browse files
committed
fix(worker): terminal WebSocket return raw DO response and allowlist headers
- Return container.fetch() Response as-is (match Cloudflare containers WS example). - Forward only handshake + X-* headers; drop full browser/CF header copy. - Rely on CloudShellTerminal defaultPort 8080 instead of switchPort. Made-with: Cursor
1 parent e0f9cbd commit 0e4749b

File tree

2 files changed

+33
-24
lines changed

2 files changed

+33
-24
lines changed

worker/effect/services.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,20 @@ function toContainerFailure(message: string, retryable: boolean) {
184184
}
185185

186186
/**
187-
* DO → container: same shape as agentcast-exploration CDP proxy — browser URL with path rewritten to
188-
* `/ws/terminal`, full handshake headers forwarded, ticket stripped from query. Session/tab identity
189-
* from validated `X-*` overrides (main.go).
187+
* DO → container: worker URL path → `/ws/terminal`, ticket stripped (JWT length / auth). Identity from
188+
* validated `X-*` (main.go). Handshake headers are allowlisted only — forwarding full browser / CF
189+
* headers has caused failed upgrades (1006); Cloudflare’s container WS example forwards the client
190+
* request as-is, but we must strip hop-by-hop and edge noise for the inner hop.
190191
*/
192+
const WS_FORWARD_HEADER_NAMES = [
193+
'upgrade',
194+
'connection',
195+
'sec-websocket-key',
196+
'sec-websocket-version',
197+
'sec-websocket-protocol',
198+
'sec-websocket-extensions',
199+
] as const;
200+
191201
export function buildContainerWebSocketRequest(
192202
clientRequest: Request,
193203
username: string,
@@ -200,21 +210,22 @@ export function buildContainerWebSocketRequest(
200210
containerUrl.searchParams.delete('sessionId');
201211
containerUrl.searchParams.delete('tabId');
202212

203-
const h = new Headers(clientRequest.headers);
213+
const h = new Headers();
214+
for (const name of WS_FORWARD_HEADER_NAMES) {
215+
const v = clientRequest.headers.get(name);
216+
if (v) {
217+
h.set(name, v);
218+
}
219+
}
204220
h.set('X-User', username);
205221
h.set('X-Session-Id', sessionId);
206222
h.set('X-Tab-Id', tabId);
207223

208-
return new Request(containerUrl.toString(), { method: 'GET', headers: h });
209-
}
210-
211-
/** Match exploration: return a fresh 101 Response with the DO `webSocket` so the client upgrade completes reliably. */
212-
export function upgradeWebSocketResponse(res: Response): Response {
213-
const ws = (res as { webSocket?: WebSocket }).webSocket;
214-
if (ws != null) {
215-
return new Response(null, { status: 101, webSocket: ws });
216-
}
217-
return res;
224+
return new Request(containerUrl.toString(), {
225+
method: 'GET',
226+
headers: h,
227+
signal: clientRequest.signal,
228+
});
218229
}
219230

220231
function runtimeAnnotations(context: RuntimeLogContext, containerId: string) {
@@ -915,10 +926,9 @@ const ContainerRuntimeLive = Layer.effect(
915926
input.sessionId,
916927
input.tabId
917928
);
918-
const switched = switchPort(containerReq, 8080);
919929
let res: Response;
920930
try {
921-
res = await ready.container.fetch(switched);
931+
res = await ready.container.fetch(containerReq);
922932
} catch (err) {
923933
console.error('[ws/terminal] DO stub.fetch threw', {
924934
containerId: ready.containerId,
@@ -938,7 +948,7 @@ const ContainerRuntimeLive = Layer.effect(
938948
const peek = await res.clone().text().catch(() => '');
939949
console.log('[ws/terminal] non-upgrade response body', peek.slice(0, 400));
940950
}
941-
return upgradeWebSocketResponse(res);
951+
return res;
942952
},
943953
catch: toContainerFailure('Container error, please retry in a moment.', true),
944954
})

worker/index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Hono } from 'hono';
22
import type { Context, ExecutionContext } from 'hono';
3-
import { Container, getContainer, switchPort } from '@cloudflare/containers';
3+
import { Container, getContainer } from '@cloudflare/containers';
44
import {
55
backupWorkspace,
66
checkpointSession,
@@ -22,7 +22,7 @@ import {
2222
updateSession,
2323
prepareTerminalForWebSocketContext,
2424
} from './effect/programs';
25-
import { buildContainerWebSocketRequest, upgradeWebSocketResponse } from './effect/services';
25+
import { buildContainerWebSocketRequest } from './effect/services';
2626
import {
2727
runJsonRoute,
2828
runRequestEffect,
@@ -512,7 +512,7 @@ const app = createApp();
512512

513513
/**
514514
* Guarded smoke route matching cloudflare/containers-demos/terminal worker:
515-
* `return await getContainer(binding).fetch(switchPort(request, 8080))` on the browser request (minus `secret` query).
515+
* `return await getContainer(binding).fetch(request)` on the browser request (minus `secret` query); DO `defaultPort` is 8080.
516516
* Enable with TERMINAL_PARITY_SECRET in deploy env + second container in alchemy.run.ts.
517517
*/
518518
async function handleTerminalParitySmoke(request: Request, env: Env): Promise<Response> {
@@ -529,8 +529,7 @@ async function handleTerminalParitySmoke(request: Request, env: Env): Promise<Re
529529
const forward = new Request(url.toString(), request);
530530
console.log('[terminal-parity] demo-shaped stub.fetch', { pathname: url.pathname });
531531
try {
532-
const res = await getContainer(ns).fetch(switchPort(forward, 8080));
533-
return upgradeWebSocketResponse(res);
532+
return await getContainer(ns).fetch(forward);
534533
} catch (err) {
535534
console.error('[terminal-parity] stub.fetch threw', err);
536535
return toErrorResponse(
@@ -566,7 +565,7 @@ async function handleTerminalWebSocket(request: Request, env: Env): Promise<Resp
566565
const inner = buildContainerWebSocketRequest(request, prep.username, prep.sessionId, prep.tabId);
567566
let res: Response;
568567
try {
569-
res = await prep.ready.container.fetch(switchPort(inner, 8080));
568+
res = await prep.ready.container.fetch(inner);
570569
} catch (err) {
571570
console.error('[ws/terminal] DO stub.fetch threw', { ms: Date.now() - t0, err });
572571
return toErrorResponse(
@@ -588,7 +587,7 @@ async function handleTerminalWebSocket(request: Request, env: Env): Promise<Resp
588587
const peek = await res.clone().text().catch(() => '');
589588
console.log('[ws/terminal] non-upgrade response body', peek.slice(0, 400));
590589
}
591-
return upgradeWebSocketResponse(res);
590+
return res;
592591
} catch (error) {
593592
return toErrorResponse(error);
594593
}

0 commit comments

Comments
 (0)