src/vs/platform/tunnel/node/tunnelProxy.ts

599 LOC · 553 covered · 46 uncovered · 76 ranges · 25 concepts · 20 introducers · 23 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/base/node/ports.ts · 211 LOCnode/ports.tstunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport CONNECT tunnels bidirectional data through a managed socket|occurrence=1 · 0 introduced LOCtunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport forwards an authenticated HTTP GET through a managed socket|occurrence=1 · 0 introduced LOCtunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport dispose disposes the managed remote socket via the adapter|occurrence=1 · 0 introduced LOCtunnelProxy.test|title=T…tunnelProxy.ts ×1 · 6 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×2 · 6 introduced LOCtunnelProxy.ts ×2tunnelProxy.test|title=TunnelProxy dispose terminates active CONNECT tunnels|occurrence=1, tunnelProxy.test|title=TunnelProxy CONNECT establishes a tunnel to the target|occurrence=1, +1 · 0 introduced LOCtunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy forwards authenticated HTTP GET to target|occurrence=1, tunnelProxy.test|title=TunnelProxy forwards authenticated HTTP POST to target|occurrence=1, +4 · 0 introduced LOCtunnelProxy.test|title=T…tunnelProxy.ts ×2 · 11 introduced LOCtunnelProxy.ts ×2tunnelProxy.ts ×5 · 21 introduced LOCtunnelProxy.ts ×5tunnelProxy.ts ×4 · 20 introduced LOCtunnelProxy.ts ×4tunnelProxy.ts ×3 · 9 introduced LOCtunnelProxy.ts ×3tunnelProxy.ts ×10 · 66 introduced LOCtunnelProxy.ts ×10tunnelProxy.ts ×1 · 14 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×1 · 1 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×2 · 9 introduced LOCtunnelProxy.ts ×2tunnelProxy.ts ×2 · 6 introduced LOCtunnelProxy.ts ×2tunnelProxy.ts ×7 · 27 introduced LOCtunnelProxy.ts ×7tunnelProxy.ts ×1 · 2 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×3 · 17 introduced LOCtunnelProxy.ts ×3tunnelProxy.ts ×1 · 4 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×1 · 8 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×5 · 11 introduced LOCtunnelProxy.ts ×5tunnelProxy.ts ×4 · 13 introduced LOCtunnelProxy.ts ×4tunnelProxy.ts ×1 · 2 introduced LOCtunnelProxy.ts ×1tunnelProxy.ts ×20 · 329 introduced LOCtunnelProxy.ts ×20tunnelProxy.test|title=TunnelProxy CONNECT establishes a tunnel to the target|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy CONNECT establishes a tunnel to the target|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy CONNECT rejects invalid port 0|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy CONNECT rejects invalid port 0|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy CONNECT rejects port > 65535|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy CONNECT rejects port > 65535|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy a reset on a pooled tunnel socket does not escalate to an uncaught exception|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy a reset on a pooled tunnel socket does not escalate to an uncaught exception|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy dispose shuts down the server|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy dispose shuts down the server|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy dispose synchronously destroys the remote tunnel socket|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy dispose synchronously destroys the remote tunnel socket|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy dispose terminates CONNECT sockets stuck waiting for the upstream tunnel|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy dispose terminates CONNECT sockets stuck waiting for the upstream tunnel|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy dispose terminates active CONNECT tunnels|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy dispose terminates active CONNECT tunnels|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy dispose terminates idle HTTPS keep-alive connections|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy dispose terminates idle HTTPS keep-alive connections|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy drainConnectionPool destroys pooled tunnel sockets|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy drainConnectionPool destroys pooled tunnel sockets|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy fails the request when the tunnel connection fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy fails the request when the tunnel connection fails|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy forwards authenticated HTTP GET to target|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy forwards authenticated HTTP GET to target|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy forwards authenticated HTTP POST to target|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy forwards authenticated HTTP POST to target|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport CONNECT tunnels bidirectional data through a managed socket|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport CONNECT tunnels bidirectional data through a managed socket|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport dispose disposes the managed remote socket via the adapter|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport dispose disposes the managed remote socket via the adapter|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport forwards an authenticated HTTP GET through a managed socket|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy managed (non-NodeSocket) transport forwards an authenticated HTTP GET through a managed socket|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy rejects CONNECT without credentials (407)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy rejects CONNECT without credentials (407)|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy rejects plain HTTP request without credentials (407)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy rejects plain HTTP request without credentials (407)|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy returns 400 for malformed URL|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy returns 400 for malformed URL|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy reuses tunnel socket for multiple requests to the same host|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy reuses tunnel socket for multiple requests to the same host|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy server uses TLS|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy server uses TLS|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy start returns a valid ITunnelProxyInfo|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy start returns a valid ITunnelProxyInfo|occurrence=1tunnelProxy.test|title=T…tunnelProxy.test|title=TunnelProxy strips hop-by-hop headers from forwarded request|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/tunnel/test/node/tunnelProxy.test|title=TunnelProxy strips hop-by-hop headers from forwarded request|occurrence=1tunnelProxy.test|title=T…Focused file · src/vs/platform/tunnel/node/tunnelProxy.ts · 599 LOCnode/tunnelProxy.ts

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- tunnelProxy.ts ×20
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import * as net from 'net';
7 > import { Duplex } from 'stream';
8 > import type * as http from 'http';
9 > import type * as https from 'https';
10 >
11 > import { findFreePortFaster } from '../../../base/node/ports.js';
12 > import { NodeSocket } from '../../../base/parts/ipc/node/ipc.net.js';
13 > import { ISocket, SocketCloseEventType } from '../../../base/parts/ipc/common/ipc.net.js';
14 > import { VSBuffer } from '../../../base/common/buffer.js';
15 > import { Limiter } from '../../../base/common/async.js';
16 > import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
17 > import { ILogService } from '../../log/common/log.js';
18 > import { ITunnelProxyInfo } from '../common/tunnelProxy.js';
19 > import { generateSelfSignedCert } from './selfSignedCert.js';
20 >
21 > /**
22 > * Maximum number of tunnel connections we establish through the remote
23 > * agent at the same time. Each new tunnel dials the loopback forwarder,
24 > * which opens a fresh multiplexed channel to the remote (crypto +
25 > * round-trips) on a single event loop. An ad-heavy page fans out dozens
26 > * of simultaneous CONNECTs to distinct hosts; left unbounded, that
27 > * stampede overflows the forwarder's accept backlog and it starts
28 > * refusing (ECONNREFUSED) and resetting (ECONNRESET) connections. This
29 > * cap smooths the burst to a rate the forwarder can absorb; excess
30 > * requests queue rather than fail.
31 > */
32 > const MAX_CONCURRENT_TUNNEL_CONNECTS = 6;
33 >
34 > /**
35 > * A function that opens a TCP tunnel to a given host:port through the
36 > * remote agent. Resolves only once the remote has confirmed the target is
37 > * reachable (via the tunnel handshake) and rejects otherwise. Returns an
38 > * object with `getSocket()`, `readEntireBuffer()`, and `dispose()` — a
39 > * subset of {@link import('../../base/parts/ipc/common/ipc.net.js').PersistentProtocol}.
40 > */
41 > export interface ITunnelConnectFn {
42 > (host: string, port: number): Promise<{ getSocket(): ISocket; readEntireBuffer(): VSBuffer; dispose(): void }>;
43 > }
44 >
45 > /**
46 > * An HTTPS proxy server that routes TCP connections through the remote
47 > * agent tunnel.
48 > *
49 > * Handles:
50 > * - **CONNECT** requests (used by Chromium for HTTPS) — establishes a
51 > * raw TCP tunnel through the remote agent.
52 > * - **Plain HTTP** requests (GET, POST, etc. with absolute URLs) —
53 > * establishes a tunnel and forwards the request.
54 > *
55 > * The server binds exclusively to `127.0.0.1` and is never exposed to
56 > * the network — this is the primary security boundary. The additional
57 > * layers below are defence-in-depth:
58 > *
59 > * - **TLS** with a self-signed certificate (generated in-memory)
60 > * prevents other local processes from passively sniffing traffic.
61 > * - **Basic proxy authentication** with randomly generated credentials
62 > * prevents other local processes from actively using the proxy.
63 > * - The certificate **fingerprint** is returned from {@link start} so
64 > * the consumer's Electron session can pin it.
65 > *
66 > * If certificate generation or server startup fails the proxy simply
67 > * does not start — the worst outcome is that the browser view falls
68 > * back to not having remote network access.
69 > */
70 > export class TunnelProxy extends Disposable {
71 >
72 > private _server: https.Server | undefined;
73 > private _http: typeof http | undefined;
74 > private _tunnelAgent: http.Agent | undefined;
75 > private _localPort: number = 0;
76 > private _credentials: { username: string; password: string } | undefined;
77 > private _expectedAuthHeader: string | undefined;
78 > private _certFingerprint: string | undefined;
79 >
80 > /**
81 > * Sockets we took over from the HTTPS server via CONNECT. Once the
82 > * CONNECT handler runs the server no longer tracks them, so
83 > * `server.close()` and `server.closeAllConnections()` won't terminate
84 > * them — we have to destroy them ourselves on dispose to release the
85 > * listening port promptly.
86 > */
87 > private readonly _connectSockets = new Set<net.Socket>();
88 >
89 > /**
90 > * The remote (tunnel) side of every active bridge — both CONNECT
91 > * tunnels and pooled plain-HTTP sockets. We destroy these explicitly
92 > * and synchronously on dispose rather than relying on the local
93 > * socket's async `'close'` to propagate `end()`; during shared-process
94 > * teardown the event loop may not get another turn to fire that
95 > * listener, which would leave the upstream tunnel socket dangling.
96 > */
97 > private readonly _remoteSockets = new Set<Duplex>();
98 >
99 > /**
100 > * Bounds how many tunnels we create concurrently through the remote
101 > * agent. Gates the setup (connect + handshake) only; once a tunnel is
102 > * established the slot is released and data piping proceeds unthrottled.
103 > */
104 > private readonly _connectLimiter = this._register(new Limiter<Awaited<ReturnType<ITunnelConnectFn>>>(MAX_CONCURRENT_TUNNEL_CONNECTS));
105 >
106 > get localPort(): number {
107 > return this._localPort;
108 > }
109 >
110 > constructor(
111 > private readonly _connectTunnel: ITunnelConnectFn,
112 > private readonly _logService: ILogService,
113 > ) {
114 > super();
115 > }
116 >
117 > async start(): Promise<ITunnelProxyInfo> {
118 > const crypto = await import('crypto');
119 > const http = await import('http');
120 > const https = await import('https');
121 >
122 > // Generate random credentials
123 > const username = crypto.randomBytes(16).toString('hex');
124 > const password = crypto.randomBytes(32).toString('hex');
125 > this._credentials = { username, password };
126 > this._expectedAuthHeader = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
127 >
128 > // Generate a self-signed certificate in memory
129 > const { key, cert, fingerprint } = await generateSelfSignedCert();
130 > this._certFingerprint = fingerprint;
131 >
132 > // Create an agent that pools tunnel sockets by host:port.
133 > this._http = http;
134 > this._tunnelAgent = this._createTunnelAgent();
135 >
136 > // HTTPS server: handles plain HTTP requests (absolute-form URLs from
137 > // Chromium when configured as a proxy) and CONNECT tunnels for HTTPS.
138 > const server = https.createServer({ key, cert }, (req, res) => this._onRequest(req, res));
139 > server.on('connect', (req, socket, head) => this._onConnect(req, socket as net.Socket, head));
140 > server.on('error', err => {
141 this._logService.error('[TunnelProxy] Server error:', err);
143 > this._server = server;
144 >
145 > const port = await findFreePortFaster(0, 2, 1000, '127.0.0.1');
146 > server.listen(port, '127.0.0.1');
147 > await new Promise<void>((resolve, reject) => {
148 > server.once('listening', resolve);
149 > server.once('error', reject);
150 > });
151 > const address = server.address() as net.AddressInfo;
152 > this._localPort = address.port;
153 > this._logService.info(`[TunnelProxy] Listening on https://127.0.0.1:${this._localPort}`);
154 >
155 > return {
156 > url: `https://127.0.0.1:${this._localPort}`,
157 > host: '127.0.0.1',
158 > port: this._localPort,
159 > credentials: this._credentials,
160 > certFingerprint: this._certFingerprint,
161 > };
162 > }
163 >
164 > override dispose(): void {
165 > // Any tunnels still queued behind the limiter are abandoned here:
166 > // disposing the limiter drops the outstanding queue without settling
167 > // those promises, so their awaiting `_onConnect`/`_createTunnelSocket`
168 > // never resumes. That's fine — we destroy every socket below, and the
169 > // local sockets those handlers would have served are torn down too, so
170 > // nothing is left waiting on a tunnel that will never arrive.
171 > for (const socket of this._connectSockets) {
172 > socket.destroy(); tunnelProxy.ts ×4
173 > }
174 > this._connectSockets.clear(); tunnelProxy.ts ×20
175 > for (const socket of this._remoteSockets) {
176 > socket.destroy(); tunnelProxy.ts ×1
177 > }
178 > this._remoteSockets.clear(); tunnelProxy.ts ×20
179 > this._tunnelAgent?.destroy();
180 > this._server?.closeAllConnections();
181 > this._server?.close();
182 > super.dispose();
183 > }
184 >
185 > /**
186 > * Verify the `Proxy-Authorization` header against our credentials.
187 > * Returns `true` if the request is authorized.
188 > */
189 > private _checkAuth(authHeader: string | undefined): boolean {
190 > return authHeader === this._expectedAuthHeader; tunnelProxy.ts ×1
191 > }
193 > /**
194 > * Create an `http.Agent` that pools tunnel sockets by target
195 > * host:port. Node calls `createConnection` only when no pooled socket
196 > * is available for the target; otherwise it reuses an existing one.
197 > */
198 > private _createTunnelAgent(): http.Agent {
199 > if (!this._http) {
200 throw new Error('HTTP module not initialized');
201 }
202 > const agent = new this._http.Agent({ keepAlive: true }); tunnelProxy.ts ×20
203 > agent.createConnection = (options, oncreate) => {
204 > const host = options.hostname || options.host || ''; tunnelProxy.ts ×10
205 > const port = Number(options.port) || 80;
206 > this._createTunnelSocket(host, port)
207 > .then(socket => oncreate?.(null, socket))
208 > .catch(err => oncreate?.(err, null!));
209 > };
210 > return agent; tunnelProxy.ts ×20
211 > }
212 >
213 > /**
214 > * Drop every pooled keep-alive tunnel socket by recreating the
215 > * agent. Called when the upstream tunnel endpoint changes: the pooled
216 > * sockets all dial the now-stale endpoint, so they would be reset en
217 > * masse once it goes away. Recreating the agent closes the idle ones
218 > * gracefully and forces subsequent requests to dial the new endpoint.
219 > */
220 > drainConnectionPool(): void {
221 > if (!this._tunnelAgent) { tunnelProxy.ts ×2
222 return; // not started yet; nothing pooled
223 }
224 > const oldAgent = this._tunnelAgent; tunnelProxy.ts ×2
225 > this._tunnelAgent = this._createTunnelAgent();
226 > oldAgent?.destroy();
227 > this._logService.trace('[TunnelProxy] Upstream endpoint changed; drained pooled tunnel sockets');
228 > }
230 > /**
231 > * Handle HTTP CONNECT requests (used for HTTPS tunneling).
232 > * Parses `host:port` from the request URL, establishes a tunnel
233 > * through the remote agent, and pipes the sockets together.
234 > */
235 > private async _onConnect(req: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> {
236 > // Track the socket from the moment the CONNECT event fires so tunnelProxy.ts ×4
237 > // dispose can tear it down even before the upstream tunnel
238 > // returns (or if auth/host validation fails). The close listener
239 > // auto-removes whether we close it here or later.
240 > this._connectSockets.add(socket);
241 > socket.on('close', () => this._connectSockets.delete(socket));
242 >
243 > if (!this._checkAuth(req.headers['proxy-authorization'])) {
244 > socket.write( tunnelProxy.ts ×1
245 > 'HTTP/1.1 407 Proxy Authentication Required\r\n' +
246 > 'Proxy-Authenticate: Basic realm="TunnelProxy"\r\n' +
247 > '\r\n'
248 > );
249 > socket.end();
250 > return;
251 > }
253 > const { host, port } = this._parseHostPort(req.url ?? '', 443); tunnelProxy.ts ×4
254 > if (!host) {
255 > socket.write('HTTP/1.1 400 Bad Request\r\n\r\n'); tunnelProxy.ts ×2
256 > socket.end();
257 > return;
258 > }
260 > this._logService.trace(`[TunnelProxy] CONNECT ${host}:${port}`);
261 >
262 > try {
263 > socket.pause();
264 >
265 > const protocol = await this._connectLimiter.queue(() => this._connectTunnel(host, port));
266 > const { stream: remoteSocket, leftover } = this._takeRemoteStream(protocol); tunnelProxy.ts ×4
267 >
268 > socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
269 >
270 > if (leftover.byteLength > 0) {
271 socket.write(leftover.buffer);
272 }
274 > if (head.length > 0) {
275 remoteSocket.write(head);
276 }
278 > this._bridgeSockets(socket, remoteSocket);
279 > } catch (err) { tunnelProxy.ts ×1
280 > this._logService.error(`[TunnelProxy] Failed to tunnel to ${host}:${port}:`, err); tunnelProxy.ts ×2
281 > socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
282 > socket.end();
283 > }
286 > /**
287 > * Handle plain HTTP requests (GET, POST, etc. with absolute URLs).
288 > *
289 > * Chromium sends proxied HTTP requests with absolute-form URLs
290 > * (e.g. `GET http://example.com/page HTTP/1.1`) and reuses keep-alive
291 > * connections to the proxy for requests to **different** hosts.
292 > *
293 > * Each request is forwarded via `http.request` using a shared
294 > * `http.Agent` that pools tunnel sockets by host:port. The agent
295 > * calls `_createTunnelSocket` only when no pooled socket is available;
296 > * otherwise it reuses an existing tunnel connection.
297 > */
298 > private async _onRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
299 > if (!this._checkAuth(req.headers['proxy-authorization'])) { tunnelProxy.ts ×5
300 > res.writeHead(407, { 'Proxy-Authenticate': 'Basic realm="TunnelProxy"' }); tunnelProxy.ts ×1
301 > res.end();
302 > return;
303 > }
305 > let parsed: URL;
306 > try {
307 > parsed = new URL(req.url ?? ''); tunnelProxy.ts ×5
308 > } catch {
309 res.writeHead(400);
310 res.end();
311 return;
312 }
314 > // Plain HTTP forwarding only — HTTPS goes through CONNECT.
315 > // In practice every HTTP/1.1 client (browsers included) uses
316 > // CONNECT for HTTPS via a proxy, so an absolute-form `https:`
317 > // URL here should never happen. Reject loudly rather than
318 > // silently misforward it as plaintext (`http.request` to either
319 > // the URL's port or default 80 would produce confusing failures
320 > // or wrong content).
321 > if (parsed.protocol !== 'http:') {
322 this._logService.warn(`[TunnelProxy] Rejecting non-HTTP forwarded request: ${req.method} ${req.url}`);
323 res.writeHead(400);
324 res.end();
325 return;
326 }
328 > const host = parsed.hostname;
329 > const port = parseInt(parsed.port, 10) || 80;
331 > if (!host) {
332 res.writeHead(400);
333 res.end();
334 return;
335 }
337 > this._logService.trace(`[TunnelProxy] ${req.method} ${host}:${port}${parsed.pathname}`);
338 >
339 > try {
340 > const http = await import('http');
341 > const path = parsed.pathname + parsed.search;
342 > const headers = { ...req.headers };
343 >
344 > // Strip hop-by-hop headers per RFC 9110 Section 7.6.1.
345 > // An intermediary MUST parse the Connection header and remove any
346 > // fields named in it, then remove Connection itself. It SHOULD
347 > // also remove other known hop-by-hop headers.
348 > const connectionTokens = (headers['connection'] ?? '')
349 > .toString() tunnelProxy.ts ×5
350 > .split(',')
351 > .map(t => t.trim().toLowerCase())
352 > .filter(t => t.length > 0);
353 > for (const token of connectionTokens) {
354 > delete headers[token]; tunnelProxy.ts ×10
355 > }
356 > delete headers['connection'];
357 > delete headers['keep-alive'];
358 > delete headers['proxy-authorization'];
359 > delete headers['proxy-connection'];
360 > delete headers['te'];
361 > delete headers['transfer-encoding'];
362 > delete headers['upgrade'];
363 >
364 > const proxyReq = http.request({
365 > agent: this._tunnelAgent,
366 > hostname: host,
367 > port,
368 > path,
369 > method: req.method,
370 > headers,
371 > }, proxyRes => {
372 > res.writeHead(proxyRes.statusCode!, proxyRes.headers); tunnelProxy.ts ×3
373 > proxyRes.pipe(res);
375 >
376 > proxyReq.on('error', err => {
377 > this._logService.error(`[TunnelProxy] Proxy request error for ${host}:${port}:`, err); tunnelProxy.ts ×2
378 > // Reset the client connection instead of returning a 502 body.
379 > // Chromium renders a 502 body as a page, whereas a transport
380 > // reset triggers `did-fail-load`, so the browser shows its
381 > // native "failed to load" error page (consistent with the
382 > // HTTPS/CONNECT path).
383 > res.destroy();
385 >
386 > req.pipe(proxyReq);
387 > } catch (err) {
388 this._logService.error(`[TunnelProxy] Failed to tunnel to ${host}:${port}:`, err);
389 // Reset the client connection so the browser shows its native
390 // "failed to load" page rather than rendering an HTTP error.
391 res.destroy();
392 }
395 > /**
396 > * Create a `net.Socket`-compatible stream backed by a remote agent
397 > * tunnel. Called by the `http.Agent` when it needs a new connection
398 > * to a given host:port (i.e. no pooled socket is available).
399 > */
400 > private async _createTunnelSocket(host: string, port: number): Promise<Duplex> {
401 > // The connect function resolves only once the remote has confirmed the tunnelProxy.ts ×10
402 > // target is reachable (via the tunnel handshake) and rejects otherwise.
403 > // A rejection here lets the http.Agent fail the request (the client
404 > // connection is reset) rather than hanging or silently returning
405 > // nothing.
406 > const protocol = await this._connectLimiter.queue(() => this._connectTunnel(host, port));
407 > const { stream: tunnelStream, leftover } = this._takeRemoteStream(protocol); tunnelProxy.ts ×3
408 >
409 > this._trackRemoteSocket(tunnelStream);
410 >
411 > if (leftover.byteLength > 0) {
412 tunnelStream.unshift(leftover.buffer);
413 }
415 > return tunnelStream;
418 > /**
419 > * Take ownership of a freshly-connected tunnel's transport as a Node
420 > * {@link Duplex} stream, together with any bytes the protocol already
421 > * buffered during the handshake (the caller routes that leftover to the
422 > * appropriate side).
423 > *
424 > * Two transports occur in practice:
425 > * - {@link NodeSocket} (classic/websocket server): unwrap the raw
426 > * `net.Socket` so we can rely on Node's native stream backpressure (via
427 > * `pipe()` and the keep-alive `http.Agent`).
428 > * - a generic {@link ISocket} (managed / exec-server connection): there is
429 > * no `net.Socket` underneath, so adapt the message-passing socket to a
430 > * {@link Duplex} ({@link RemoteSocketStream}).
431 > */
432 > private _takeRemoteStream(protocol: { getSocket(): ISocket; readEntireBuffer(): VSBuffer; dispose(): void }): { stream: Duplex; leftover: VSBuffer } {
433 > const remoteSocket = protocol.getSocket(); tunnelProxy.ts ×3
434 >
435 > if (remoteSocket instanceof NodeSocket) {
436 > // Take ownership of the raw socket, detaching NodeSocket's own tunnelProxy.ts ×1
437 > // listeners. NodeSocket installs an 'error' listener that routes
438 > // every non-EPIPE error through onUnexpectedError, which the host
439 > // process logs as an "uncaught exception". When the upstream tunnel
440 > // endpoint dies, every pooled/active tunnel socket is reset at once
441 > // - that ECONNRESET is expected teardown here, not an unexpected
442 > // error. We bridge the raw socket ourselves (attaching our own
443 > // 'error' handlers), so NodeSocket's routing must be removed.
444 > const socket = remoteSocket.socket;
445 > const leftover = protocol.readEntireBuffer();
446 > remoteSocket.dispose(false);
447 > protocol.dispose();
448 > return { stream: socket, leftover };
449 > }
451 > // Generic ISocket (e.g. a managed/exec-server connection). Read the
452 > // buffered leftover and detach the protocol's reader/writer before the
453 > // adapter starts consuming the socket, so subsequent messages reach the
454 > // adapter exactly once. This all runs synchronously, so no message can
455 > // arrive in the gap and be lost.
456 > const leftover = protocol.readEntireBuffer();
457 > protocol.dispose();
458 > return { stream: new RemoteSocketStream(remoteSocket), leftover };
461 > /**
462 > * Parse a `host:port` string. Falls back to `defaultPort` when the
463 > * port component is missing. Returns an empty host when the address
464 > * is empty or the port is outside the valid TCP range (1-65535), per
465 > * RFC 9110 section 9.3.6 ("A server MUST reject a CONNECT request that
466 > * targets an empty or invalid port number").
467 > */
468 > private _parseHostPort(address: string, defaultPort: number): { host: string; port: number } {
469 > let host: string; tunnelProxy.ts ×7
470 > let port: number;
471 >
472 > // Handle IPv6 bracket notation [::1]:port
473 > const bracketMatch = /^\[(?<host>[^\]]+)\]:(?<port>\d+)$/.exec(address);
474 > if (bracketMatch?.groups) {
475 host = bracketMatch.groups['host'];
476 port = parseInt(bracketMatch.groups['port'], 10);
477 > } else { tunnelProxy.ts ×7
478 > const bracketOnly = /^\[(?<host>[^\]]+)\]$/.exec(address);
479 > if (bracketOnly?.groups) {
480 host = bracketOnly.groups['host'];
481 port = defaultPort;
482 > } else { tunnelProxy.ts ×7
483 > const lastColon = address.lastIndexOf(':');
484 > if (lastColon === -1) {
485 host = address;
486 port = defaultPort;
487 > } else { tunnelProxy.ts ×7
488 > const maybePort = parseInt(address.substring(lastColon + 1), 10);
489 > if (isNaN(maybePort)) {
490 // Likely an IPv6 address without brackets
491 host = address;
492 port = defaultPort;
493 > } else { tunnelProxy.ts ×7
494 > host = address.substring(0, lastColon);
495 > port = maybePort;
496 > }
497 > }
498 > }
499 > }
500 >
501 > // Validate port range
502 > if (port < 1 || port > 65535) {
503 > return { host: '', port: 0 }; tunnelProxy.ts ×2
504 > }
506 > return { host, port };
509 > private _bridgeSockets(localSocket: net.Socket, remoteSocket: Duplex): void {
510 > this._trackRemoteSocket(remoteSocket); tunnelProxy.ts ×4
511 > remoteSocket.on('end', () => localSocket.end());
512 > remoteSocket.on('close', () => localSocket.end());
513 > remoteSocket.on('error', () => localSocket.destroy());
514 > localSocket.on('end', () => remoteSocket.end());
515 > localSocket.on('close', () => remoteSocket.end());
516 > localSocket.on('error', () => remoteSocket.destroy());
517 >
518 > remoteSocket.pipe(localSocket);
519 > localSocket.pipe(remoteSocket);
520 > }
522 > /**
523 > * Track a remote tunnel socket so {@link dispose} can tear it down
524 > * synchronously. The socket auto-removes itself once closed.
525 > */
526 > private _trackRemoteSocket(socket: Duplex): void {
527 > this._remoteSockets.add(socket); tunnelProxy.ts ×3
528 >
529 > // Once we detach NodeSocket's listeners (see _takeRemoteStream)
530 > // the raw socket has no 'error' handler of its own. A net.Socket
531 > // that emits 'error' without a listener throws as a genuine
532 > // uncaught exception, so every socket we own must have one.
533 > // Destroying on error tears the socket down quietly and lets the
534 > // agent evict it from the pool. (CONNECT bridges attach an
535 > // additional handler in _bridgeSockets; a second listener is
536 > // harmless.)
537 > socket.on('error', () => socket.destroy());
538 > socket.on('close', () => this._remoteSockets.delete(socket));
539 > }
541 >
542 > /**
543 > * Adapts a generic {@link ISocket} (such as a managed / exec-server
544 > * connection, which has no underlying `net.Socket`) to a Node {@link Duplex}
545 > * stream, so the {@link TunnelProxy} can pipe and pool it exactly like the raw
546 > * socket it extracts from a {@link NodeSocket}.
547 > */
548 > class RemoteSocketStream extends Duplex {
549 >
550 > private readonly _disposables = new DisposableStore();
551 >
552 > constructor(private readonly _socket: ISocket) {
553 > super(); tunnelProxy.ts ×5
554 > this._disposables.add(this._socket.onData(data => this.push(data.buffer)));
555 > this._disposables.add(this._socket.onEnd(() => this.push(null)));
556 > this._disposables.add(this._socket.onClose(e => {
557 // The transport is fully closed, so tear the stream down: this emits
558 // 'close' (removing it from the proxy's socket set and evicting it from
559 // the http.Agent pool) and disposes the underlying ISocket via _destroy.
560 // A clean close carries no error.
561 this.destroy(e?.type === SocketCloseEventType.NodeSocketCloseEvent ? e.error : undefined);
562 > })); tunnelProxy.ts ×5
563 > }
565 > // The keep-alive http.Agent pools tunnel sockets and calls net.Socket-only
566 > // transport knobs on them (setKeepAlive/ref/unref, and setTimeout/setNoDelay
567 > // while wiring a request) when parking or reusing a connection. A generic
568 > // ISocket has no such knobs, so expose no-op shims to keep the agent happy;
569 > // otherwise freeing a pooled managed socket throws (e.g.
570 > // "socket.setKeepAlive is not a function").
571 > setKeepAlive(): this { return this; }
572 > setNoDelay(): this { return this; }
573 > setTimeout(): this { return this; }
574 > ref(): this { return this; }
575 > unref(): this { return this; }
576 >
577 > override _read(): void {
578 > // Data is delivered through the onData listener; nothing to pull here. tunnelProxy.ts ×5
579 > }
581 > override _write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
582 > this._socket.write(VSBuffer.wrap(chunk)); tunnelProxy.ts ×1
583 > // Respect backpressure: defer completion until the socket has drained its
584 > // buffer so a fast producer cannot queue unbounded data on a slow managed /
585 > // exec-server transport.
586 > this._socket.drain().then(() => callback(), err => callback(err));
587 > }
589 > override _final(callback: (error?: Error | null) => void): void {
590 this._socket.end();
591 callback();
592 }
594 > override _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
595 > this._disposables.dispose(); tunnelProxy.ts ×5
596 > this._socket.dispose();
597 > callback(error);
598 > }