///
// Service Worker: intercepts HTTP requests to /vs//* and forwards them
// to the controlling page (which owns the WebRTC data channel) over a
// MessageChannel, streaming the response back. WebSocket connections can't be
// intercepted here, so we inject a bootstrap script into VS Code's HTML that
// overrides window.WebSocket inside the iframe (see tunnel-websocket.ts).
import { cdnProxyBase, isProxiableCdnHost } from "./config";
const sw = self as unknown as ServiceWorkerGlobalScope;
const VS_PREFIX = /^\/vs\/([^/]+)(\/.*)?$/;
const CDN_CACHE = "codehost-cdn-v1";
const VS_STATIC_CACHE = "codehost-vs-static-v1";
// VS Code's own immutable assets: /stable-/static/** is content-
// addressed by the commit hash and identical across daemons, so it's cached
// once per browser instead of crossing the WebRTC tunnel on every load. The
// cache key strips the per-process /vs/ prefix.
const VS_STATIC = /^\/(stable-[0-9a-f]{40})\/static\//;
sw.addEventListener("install", () => sw.skipWaiting());
sw.addEventListener("activate", (e) => e.waitUntil(sw.clients.claim()));
sw.addEventListener("fetch", (event: FetchEvent) => {
const url = new URL(event.request.url);
if (url.origin !== sw.location.origin) {
// VS Code's product CDN (main.vscode-cdn.net, ...) sends no CORS headers, so
// its cross-origin fetches fail in our iframe. Route them through the
// signaling Worker, which re-serves them with permissive CORS.
if (event.request.method === "GET" && isProxiableCdnHost(url.hostname)) {
event.respondWith(proxyCdn(url));
}
return; // all other cross-origin requests pass through untouched
}
// Serve the iframe bootstrap from the SW itself (same-origin, CSP 'self').
if (url.pathname === "/__codehost/bootstrap.js") {
event.respondWith(bootstrapResponse());
return;
}
const m = url.pathname.match(VS_PREFIX);
if (!m) return; // let the network/Pages handle the discovery app itself
const peerId = m[1];
const rest = (m[2] ?? "/") + url.search;
if (event.request.method === "GET" && !event.request.headers.has("range") && VS_STATIC.test(rest)) {
event.respondWith(cachedStatic(event.request, peerId, rest));
return;
}
event.respondWith(proxyOverTunnel(event.request, peerId));
});
/** Cache-first for the immutable VS Code static assets; on a cache miss the
* tunnel fills it, and assets of other (older) commits are evicted. */
async function cachedStatic(request: Request, peerId: string, rest: string): Promise {
const key = `${sw.location.origin}/__codehost/vs-static${rest}`;
const cache = await caches.open(VS_STATIC_CACHE);
const hit = await cache.match(key);
if (hit) return hit;
const res = await proxyOverTunnel(request, peerId);
if (res.status === 200) {
void cache.put(key, res.clone()).catch(() => {});
void evictOtherCommits(cache, rest).catch(() => {});
}
return res;
}
async function evictOtherCommits(cache: Cache, rest: string): Promise {
const commit = rest.match(VS_STATIC)?.[1];
if (!commit) return;
for (const req of await cache.keys()) {
if (!new URL(req.url).pathname.includes(`/${commit}/`)) void cache.delete(req);
}
}
/**
* Fetch an allow-listed VS Code CDN asset through the signaling Worker's /cdn
* route (which adds CORS), caching the result so each asset crosses to the
* Worker once per browser rather than on every request.
*/
async function proxyCdn(url: URL): Promise {
const target = `${cdnProxyBase(sw.location.hostname, sw.location.protocol)}/cdn/${url.hostname}${url.pathname}${url.search}`;
const cache = await caches.open(CDN_CACHE);
const hit = await cache.match(target);
if (hit) return hit;
try {
const res = await fetch(target);
if (res.ok) void cache.put(target, res.clone()).catch(() => {});
return res;
} catch (err) {
return new Response(`cdn proxy error: ${String(err)}`, { status: 502 });
}
}
async function proxyOverTunnel(request: Request, peerId: string): Promise {
const client = await pickClient();
if (!client) return new Response("no codehost page open", { status: 502 });
const url = new URL(request.url);
const headers: Record = {};
request.headers.forEach((v, k) => (headers[k] = v));
// Tell the daemon our public host so VS Code advertises it as the client's
// remoteAuthority and builds same-origin resource URLs (vscode-remote-resource,
// extension grammars) that route back through the tunnel — instead of the
// unreachable 127.0.0.1: it bakes in when it only sees the local host.
headers["x-forwarded-host"] = sw.location.host;
const bodyBuf =
request.method === "GET" || request.method === "HEAD"
? undefined
: new Uint8Array(await request.arrayBuffer());
const isDocument = request.mode === "navigate" || request.destination === "document";
return new Promise((resolve) => {
const mc = new MessageChannel();
let resolved = false;
mc.port1.onmessage = (ev) => {
const msg = ev.data;
if (msg.type === "head") {
const stream = new ReadableStream({
start(controller) {
mc.port1.onmessage = (e2) => {
const m2 = e2.data;
if (m2.type === "body") controller.enqueue(new Uint8Array(m2.chunk));
else if (m2.type === "end") controller.close();
else if (m2.type === "error") controller.error(new Error(m2.message));
};
},
});
// Inject the WS-shim bootstrap into VS Code's HTML documents.
const headers = new Headers(msg.headers);
const ct = headers.get("content-type") ?? "";
if (isDocument && ct.includes("text/html")) {
resolved = true;
resolve(injectBootstrap(stream, msg, peerId, headers));
return;
}
resolved = true;
resolve(new Response(stream, { status: msg.status, statusText: msg.statusText, headers }));
} else if (msg.type === "error" && !resolved) {
resolved = true;
resolve(new Response(`tunnel error: ${msg.message}`, { status: 502 }));
}
};
client.postMessage(
{
type: "tunnel-fetch",
peerId,
method: request.method,
path: url.pathname + url.search,
headers,
body: bodyBuf,
},
[mc.port2, ...(bodyBuf ? [bodyBuf.buffer] : [])],
);
});
}
// Strip CSP so our injected same-origin bootstrap can run, and prepend the
// bootstrap `;
const html = raw.includes("") ? raw.replace("", `${tag}`) : tag + raw;
headers.delete("content-security-policy");
headers.delete("content-security-policy-report-only");
headers.set("content-length", String(new TextEncoder().encode(html).byteLength));
return new Response(html, { status: head.status, statusText: head.statusText, headers });
}
async function pickClient(): Promise {
const all = await sw.clients.matchAll({ type: "window", includeUncontrolled: true });
// Prefer the top-level discovery page (not the VS Code iframe).
const top = all.find((c) => !new URL(c.url).pathname.startsWith("/vs/"));
return top ?? all[0] ?? null;
}
function bootstrapResponse(): Response {
// Runs inside the VS Code iframe. The parent page exposes its TunnelClient
// factory at window.parent.__codehostMakeWS (same-origin), which returns a
// WebSocket-compatible class bound to the right peer + base path.
const js = `(() => {
const el = document.currentScript;
const base = el && el.getAttribute('data-base');
const peer = el && el.getAttribute('data-peer');
try {
const make = window.parent.__codehostMakeWS;
if (make && base && peer) {
const Shim = make(peer, base);
if (Shim) window.WebSocket = Shim;
}
} catch (e) { console.error('[codehost] WS shim install failed', e); }
})();`;
return new Response(js, {
headers: { "content-type": "text/javascript", "cache-control": "no-store" },
});
}