Skip to content

Commit 5aecb0e

Browse files
committed
refactor: remove EmbedProxy — iframe loads directly from Coder URL
The local reverse proxy was added to work around VS Code's webview sandbox blocking script execution in nested cross-origin iframes. Testing confirms the sandbox restriction does not apply when the iframe loads directly from the Coder server URL, so the proxy is unnecessary complexity. Auth continues to flow via postMessage+setSessionToken (no cookies, no CSRF, no proxy header injection).
1 parent b341101 commit 5aecb0e

File tree

1 file changed

+10
-123
lines changed

1 file changed

+10
-123
lines changed

src/webviews/chat/chatPanelProvider.ts

Lines changed: 10 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,14 @@
1-
import * as http from "node:http";
21
import { randomBytes } from "node:crypto";
3-
import { URL } from "node:url";
42
import * as vscode from "vscode";
53

64
import { type CoderApi } from "../../api/coderApi";
75
import { type Logger } from "../../logging/logger";
86

97
/**
10-
* A local reverse proxy that forwards requests to the Coder server.
11-
* This exists solely to work around VS Code's webview sandbox which
12-
* blocks script execution in nested cross-origin iframes. By serving
13-
* through a local proxy the iframe gets its own browsing context and
14-
* scripts execute normally.
8+
* Provides a webview that embeds the Coder agent chat UI.
9+
* Authentication flows through postMessage:
1510
*
16-
* The proxy does NOT inject auth headers — authentication is handled
17-
* entirely via the postMessage bootstrap flow.
18-
*/
19-
class EmbedProxy implements vscode.Disposable {
20-
private server?: http.Server;
21-
private _port = 0;
22-
23-
constructor(
24-
private readonly coderUrl: string,
25-
private readonly logger: Logger,
26-
) {}
27-
28-
get port(): number {
29-
return this._port;
30-
}
31-
32-
async start(): Promise<number> {
33-
const target = new URL(this.coderUrl);
34-
35-
this.server = http.createServer((req, res) => {
36-
const options: http.RequestOptions = {
37-
hostname: target.hostname,
38-
port: target.port || 80,
39-
path: req.url,
40-
method: req.method,
41-
headers: {
42-
...req.headers,
43-
host: target.host,
44-
},
45-
};
46-
47-
const proxyReq = http.request(options, (proxyRes) => {
48-
res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
49-
proxyRes.pipe(res, { end: true });
50-
});
51-
52-
proxyReq.on("error", (err) => {
53-
this.logger.warn("Embed proxy request error", err);
54-
res.writeHead(502);
55-
res.end("Bad Gateway");
56-
});
57-
58-
req.pipe(proxyReq, { end: true });
59-
});
60-
61-
return new Promise<number>((resolve, reject) => {
62-
this.server!.listen(0, "127.0.0.1", () => {
63-
const addr = this.server!.address();
64-
if (typeof addr === "object" && addr !== null) {
65-
this._port = addr.port;
66-
this.logger.info(
67-
`Embed proxy listening on 127.0.0.1:${this._port}`,
68-
);
69-
resolve(this._port);
70-
} else {
71-
reject(new Error("Failed to bind embed proxy"));
72-
}
73-
});
74-
this.server!.on("error", reject);
75-
});
76-
}
77-
78-
dispose(): void {
79-
this.server?.close();
80-
}
81-
}
82-
83-
/**
84-
* Provides a webview that embeds the Coder agent chat UI via a local
85-
* proxy. Authentication flows through postMessage:
86-
*
87-
* 1. The iframe loads /agents/{id}/embed through the proxy.
11+
* 1. The iframe loads /agents/{id}/embed on the Coder server.
8812
* 2. The embed page detects the user is signed out and sends
8913
* { type: "coder:vscode-ready" } to window.parent.
9014
* 3. Our webview relays this to the extension host.
@@ -101,7 +25,6 @@ export class ChatPanelProvider
10125

10226
private view?: vscode.WebviewView;
10327
private disposables: vscode.Disposable[] = [];
104-
private proxy?: EmbedProxy;
10528
private agentId: string | undefined;
10629

10730
constructor(
@@ -150,23 +73,8 @@ export class ChatPanelProvider
15073
}),
15174
);
15275

153-
this.proxy = new EmbedProxy(coderUrl, this.logger);
154-
this.disposables.push(this.proxy);
155-
156-
try {
157-
const port = await this.proxy.start();
158-
const proxyOrigin = `http://127.0.0.1:${port}`;
159-
const embedUrl = `${proxyOrigin}/agents/${this.agentId}/embed`;
160-
webviewView.webview.html = this.getIframeHtml(
161-
embedUrl,
162-
proxyOrigin,
163-
);
164-
} catch (err) {
165-
this.logger.error("Failed to start embed proxy", err);
166-
webviewView.webview.html = this.getErrorHtml(
167-
"Failed to start embed proxy.",
168-
);
169-
}
76+
const embedUrl = `${coderUrl}/agents/${this.agentId}/embed`;
77+
webviewView.webview.html = this.getIframeHtml(embedUrl, coderUrl);
17078

17179
webviewView.onDidDispose(() => this.disposeInternals());
17280
}
@@ -189,36 +97,15 @@ export class ChatPanelProvider
18997

19098
this.disposeInternals();
19199

192-
this.proxy = new EmbedProxy(coderUrl, this.logger);
193-
this.disposables.push(this.proxy);
194-
195100
this.disposables.push(
196101
this.view.webview.onDidReceiveMessage((msg: unknown) => {
197102
this.handleMessage(msg);
198103
}),
199104
);
200105

201-
this.proxy
202-
.start()
203-
.then((port) => {
204-
const proxyOrigin = `http://127.0.0.1:${port}`;
205-
const embedUrl = `${proxyOrigin}/agents/${this.agentId}/embed`;
206-
if (this.view) {
207-
this.view.webview.options = { enableScripts: true };
208-
this.view.webview.html = this.getIframeHtml(
209-
embedUrl,
210-
proxyOrigin,
211-
);
212-
}
213-
})
214-
.catch((err) => {
215-
this.logger.error("Failed to restart embed proxy", err);
216-
if (this.view) {
217-
this.view.webview.html = this.getErrorHtml(
218-
"Failed to start embed proxy.",
219-
);
220-
}
221-
});
106+
this.view.webview.options = { enableScripts: true };
107+
const embedUrl = `${coderUrl}/agents/${this.agentId}/embed`;
108+
this.view.webview.html = this.getIframeHtml(embedUrl, coderUrl);
222109
}
223110

224111
private handleMessage(message: unknown): void {
@@ -242,7 +129,7 @@ export class ChatPanelProvider
242129
}
243130
}
244131

245-
private getIframeHtml(embedUrl: string, proxyOrigin: string): string {
132+
private getIframeHtml(embedUrl: string, allowedOrigin: string): string {
246133
const nonce = randomBytes(16).toString("base64");
247134

248135
return /* html */ `<!DOCTYPE html>
@@ -251,7 +138,7 @@ export class ChatPanelProvider
251138
<meta charset="UTF-8">
252139
<meta http-equiv="Content-Security-Policy"
253140
content="default-src 'none';
254-
frame-src ${proxyOrigin};
141+
frame-src ${allowedOrigin};
255142
script-src 'nonce-${nonce}';
256143
style-src 'unsafe-inline';">
257144
<meta name="viewport" content="width=device-width, initial-scale=1.0">

0 commit comments

Comments
 (0)