@cluesmith/codev 3.3.6 → 3.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-farm/lib/ide-prefix.d.ts +10 -0
- package/dist/agent-farm/lib/ide-prefix.d.ts.map +1 -0
- package/dist/agent-farm/lib/ide-prefix.js +13 -0
- package/dist/agent-farm/lib/ide-prefix.js.map +1 -0
- package/dist/agent-farm/lib/ide-record.d.ts +14 -2
- package/dist/agent-farm/lib/ide-record.d.ts.map +1 -1
- package/dist/agent-farm/lib/ide-record.js +26 -2
- package/dist/agent-farm/lib/ide-record.js.map +1 -1
- package/dist/agent-farm/servers/ide-forward.d.ts +57 -5
- package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -1
- package/dist/agent-farm/servers/ide-forward.js +263 -31
- package/dist/agent-farm/servers/ide-forward.js.map +1 -1
- package/dist/agent-farm/servers/ide-server.d.ts.map +1 -1
- package/dist/agent-farm/servers/ide-server.js +4 -2
- package/dist/agent-farm/servers/ide-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +99 -6
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +11 -7
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +54 -19
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +144 -27
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/ide-extension/codev-vscode.vsix +0 -0
- package/ide-extension/manifest.json +1 -1
- package/package.json +4 -4
- package/skeleton/resources/commands/agent-farm.md +31 -4
- package/templates/tower.html +78 -3
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IDE forward prefix (Issue #1668 §D1) and its matcher. Dependency-free on
|
|
3
|
+
* purpose: the auth seam (`utils/server-utils.ts`) imports it, and it must stay
|
|
4
|
+
* a light leaf (Issue #1869). `ide-record.ts` re-exports both.
|
|
5
|
+
*/
|
|
6
|
+
/** The fixed Tower prefix that routes to the IDE server (Issue #1668 §D1). */
|
|
7
|
+
export declare const IDE_DEFAULT_PREFIX = "/ide/";
|
|
8
|
+
/** True if `pathname` targets the IDE forward prefix (`/ide` or `/ide/…`, never `/ideas`). */
|
|
9
|
+
export declare function matchIdePrefix(pathname: string): boolean;
|
|
10
|
+
//# sourceMappingURL=ide-prefix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ide-prefix.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-prefix.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGxD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IDE forward prefix (Issue #1668 §D1) and its matcher. Dependency-free on
|
|
3
|
+
* purpose: the auth seam (`utils/server-utils.ts`) imports it, and it must stay
|
|
4
|
+
* a light leaf (Issue #1869). `ide-record.ts` re-exports both.
|
|
5
|
+
*/
|
|
6
|
+
/** The fixed Tower prefix that routes to the IDE server (Issue #1668 §D1). */
|
|
7
|
+
export const IDE_DEFAULT_PREFIX = '/ide/';
|
|
8
|
+
/** True if `pathname` targets the IDE forward prefix (`/ide` or `/ide/…`, never `/ideas`). */
|
|
9
|
+
export function matchIdePrefix(pathname) {
|
|
10
|
+
const bare = IDE_DEFAULT_PREFIX.replace(/\/$/, ''); // "/ide"
|
|
11
|
+
return pathname === bare || pathname.startsWith(IDE_DEFAULT_PREFIX);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=ide-prefix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ide-prefix.js","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-prefix.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C,8FAA8F;AAC9F,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,MAAM,IAAI,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS;IAC7D,OAAO,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* never hand-edited. Tower is the sole writer; `afx ide` drives it through the
|
|
13
13
|
* local `/api/ide` endpoint.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
export declare const IDE_DEFAULT_PREFIX = "/ide/";
|
|
15
|
+
export { IDE_DEFAULT_PREFIX, matchIdePrefix } from './ide-prefix.js';
|
|
17
16
|
/** Default port the local IDE server listens on when `--port` is not given. */
|
|
18
17
|
export declare const IDE_DEFAULT_PORT = 8200;
|
|
19
18
|
/**
|
|
@@ -60,11 +59,24 @@ export declare function deleteIdeRecord(): void;
|
|
|
60
59
|
* Tower injects it at the forward instead.
|
|
61
60
|
*/
|
|
62
61
|
export declare function readIdeConnectionToken(): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Write a fresh connection token (randomBytes(32) hex, 0600), replacing any
|
|
64
|
+
* existing one, and return it (Issue #1869). Called before every IDE server
|
|
65
|
+
* SPAWN (never on adopt: a live server keeps the token it was started with), so
|
|
66
|
+
* a token that ever leaked into a browser jar is valid only for that server's
|
|
67
|
+
* uptime. Tower reads the token from this file on every forwarded request and
|
|
68
|
+
* overwrites any browser-held copy, so rotation breaks no client.
|
|
69
|
+
*/
|
|
70
|
+
export declare function rotateIdeConnectionToken(): string;
|
|
63
71
|
/**
|
|
64
72
|
* Return the Tower-owned connection token, generating and persisting one (0600)
|
|
65
73
|
* on first use. This token is the IDE server's own inner auth layer (passed via
|
|
66
74
|
* `--connection-token-file`); it is NOT a substitute for Tower's key check on
|
|
67
75
|
* the forward (Issue #1668 §D2).
|
|
76
|
+
*
|
|
77
|
+
* TEST SEAM: production code no longer calls this. Every spawn uses
|
|
78
|
+
* `rotateIdeConnectionToken` (Issue #1869), and the forward only reads the
|
|
79
|
+
* file. Tests use it to provision a token without forcing a new one.
|
|
68
80
|
*/
|
|
69
81
|
export declare function ensureIdeConnectionToken(): string;
|
|
70
82
|
//# sourceMappingURL=ide-record.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ide-record.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;
|
|
1
|
+
{"version":3,"file":"ide-record.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAiBH,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAErE,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAKrC;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,yGAAyG;IACzG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,yDAAyD;AACzD,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,eAAe,GAAG,IAAI,CA2BtD;AAOD,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAQ5D;AAED,gEAAgE;AAChE,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAKtD;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CASjD;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAajD"}
|
|
@@ -16,8 +16,10 @@ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, chmodSy
|
|
|
16
16
|
import { randomBytes } from 'node:crypto';
|
|
17
17
|
import { resolve } from 'node:path';
|
|
18
18
|
import { AGENT_FARM_DIR } from './tower-client.js';
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// The prefix and its matcher live in a dependency-free module so the auth seam
|
|
20
|
+
// (`utils/server-utils.ts`) can use them without importing this file's
|
|
21
|
+
// filesystem and tower-client dependencies (Issue #1869).
|
|
22
|
+
export { IDE_DEFAULT_PREFIX, matchIdePrefix } from './ide-prefix.js';
|
|
21
23
|
/** Default port the local IDE server listens on when `--port` is not given. */
|
|
22
24
|
export const IDE_DEFAULT_PORT = 8200;
|
|
23
25
|
const IDE_RECORD_FILENAME = 'ide-server.json';
|
|
@@ -101,11 +103,33 @@ export function readIdeConnectionToken() {
|
|
|
101
103
|
const token = readFileSync(path, 'utf-8').trim();
|
|
102
104
|
return token.length > 0 ? token : null;
|
|
103
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Write a fresh connection token (randomBytes(32) hex, 0600), replacing any
|
|
108
|
+
* existing one, and return it (Issue #1869). Called before every IDE server
|
|
109
|
+
* SPAWN (never on adopt: a live server keeps the token it was started with), so
|
|
110
|
+
* a token that ever leaked into a browser jar is valid only for that server's
|
|
111
|
+
* uptime. Tower reads the token from this file on every forwarded request and
|
|
112
|
+
* overwrites any browser-held copy, so rotation breaks no client.
|
|
113
|
+
*/
|
|
114
|
+
export function rotateIdeConnectionToken() {
|
|
115
|
+
const path = getIdeConnectionTokenPath();
|
|
116
|
+
if (!existsSync(AGENT_FARM_DIR)) {
|
|
117
|
+
mkdirSync(AGENT_FARM_DIR, { recursive: true, mode: 0o700 });
|
|
118
|
+
}
|
|
119
|
+
const token = randomBytes(32).toString('hex');
|
|
120
|
+
writeFileSync(path, token + '\n', { mode: 0o600 });
|
|
121
|
+
chmodSync(path, 0o600);
|
|
122
|
+
return token;
|
|
123
|
+
}
|
|
104
124
|
/**
|
|
105
125
|
* Return the Tower-owned connection token, generating and persisting one (0600)
|
|
106
126
|
* on first use. This token is the IDE server's own inner auth layer (passed via
|
|
107
127
|
* `--connection-token-file`); it is NOT a substitute for Tower's key check on
|
|
108
128
|
* the forward (Issue #1668 §D2).
|
|
129
|
+
*
|
|
130
|
+
* TEST SEAM: production code no longer calls this. Every spawn uses
|
|
131
|
+
* `rotateIdeConnectionToken` (Issue #1869), and the forward only reads the
|
|
132
|
+
* file. Tests use it to provision a token without forcing a new one.
|
|
109
133
|
*/
|
|
110
134
|
export function ensureIdeConnectionToken() {
|
|
111
135
|
const path = getIdeConnectionTokenPath();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ide-record.js","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,YAAY,EACZ,aAAa,EACb,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,
|
|
1
|
+
{"version":3,"file":"ide-record.js","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-record.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import http from 'node:http';
|
|
16
16
|
import type net from 'node:net';
|
|
17
|
+
import { matchIdePrefix } from '../lib/ide-record.js';
|
|
17
18
|
/** The tunnel's public authority, for stamping `x-forwarded-*` on tunnel-borne requests. */
|
|
18
19
|
export interface PublicAuthority {
|
|
19
20
|
host: string;
|
|
@@ -21,11 +22,20 @@ export interface PublicAuthority {
|
|
|
21
22
|
prefix: string;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
* preserving every
|
|
26
|
-
* `vscode-tkn` (Issue #
|
|
25
|
+
* Set the IDE connection token in a Cookie header value as `vscode-tkn=…`,
|
|
26
|
+
* preserving every other cookie. Tower's token REPLACES any incoming
|
|
27
|
+
* `vscode-tkn` (Issue #1869): Tower is the sole legitimate source, so a
|
|
28
|
+
* browser-held copy (a hand-built `?tkn=` URL, a pre-fix session, a token
|
|
29
|
+
* stale after rotation) is never authoritative. Exported for testing.
|
|
27
30
|
*/
|
|
28
31
|
export declare function mergeConnectionTokenCookie(existing: string | string[] | undefined, token: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Remove one cookie (exact name) from a Cookie header value, preserving every
|
|
34
|
+
* other pair in order. Accepts the string | string[] shape. Returns '' when no
|
|
35
|
+
* pair remains. Used to keep Tower's forward cookie (Issue #1869) away from the
|
|
36
|
+
* IDE server, the way the key header is stripped. Exported for testing.
|
|
37
|
+
*/
|
|
38
|
+
export declare function stripCookie(existing: string | string[] | undefined, name: string): string;
|
|
29
39
|
/**
|
|
30
40
|
* Derive the tunnel's public authority from the cloud config, matching the
|
|
31
41
|
* `accessUrl` Tower advertises (`tower-tunnel.ts`: `${server_url}/t/${tower_name}/`).
|
|
@@ -43,8 +53,44 @@ export declare function getPublicAuthority(): PublicAuthority | null;
|
|
|
43
53
|
* Pure and exported for unit testing.
|
|
44
54
|
*/
|
|
45
55
|
export declare function buildForwardHeaders(incoming: http.IncomingHttpHeaders, port: number, publicAuthority: PublicAuthority | null, connectionToken: string | null): Record<string, string | string[]>;
|
|
46
|
-
/** True if `pathname` targets the IDE forward prefix
|
|
47
|
-
export
|
|
56
|
+
/** True if `pathname` targets the IDE forward prefix; defined beside the prefix constant. */
|
|
57
|
+
export { matchIdePrefix };
|
|
58
|
+
/**
|
|
59
|
+
* Drop every `vscode-tkn` cookie the IDE server tries to set in the browser
|
|
60
|
+
* (Issue #1869). The server answers an authenticated request with
|
|
61
|
+
* `Set-Cookie: vscode-tkn=<connection token>` (7 days, SameSite=Lax); cookies
|
|
62
|
+
* are not port-scoped, so a browser holding it could open the remote protocol
|
|
63
|
+
* on the IDE server's own port from any localhost page, bypassing Tower. Tower
|
|
64
|
+
* injects the token on every forwarded request, so the browser never needs it.
|
|
65
|
+
* Returns undefined when nothing remains. Exported for testing.
|
|
66
|
+
*/
|
|
67
|
+
export declare function stripConnectionTokenSetCookie(value: string | string[] | undefined): string[] | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Copy response headers, dropping hop-by-hop; preserves `Cache-Control` /
|
|
70
|
+
* `ETag`. With `stripTokenCookie` (a LOCAL request, Issue #1869) it also drops
|
|
71
|
+
* the IDE server's `vscode-tkn` Set-Cookie; the tunnel path relays it unchanged
|
|
72
|
+
* (the cloud variant is a separate follow-up). Exported for testing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function copyResponseHeaders(headers: http.IncomingHttpHeaders, stripTokenCookie: boolean): Record<string, string | string[] | number>;
|
|
75
|
+
/** Largest workbench page Tower will buffer to rewrite; anything bigger streams unchanged. */
|
|
76
|
+
export declare const MAX_WORKBENCH_HTML_BYTES: number;
|
|
77
|
+
/**
|
|
78
|
+
* Hand the workbench its connection token through its own configuration
|
|
79
|
+
* instead of the browser cookie jar (Issue #1869). The workbench takes
|
|
80
|
+
* `connectionToken` from its configuration first and falls back to the
|
|
81
|
+
* `vscode-tkn` cookie, which Tower strips locally because a cookie is readable
|
|
82
|
+
* by every localhost port. Same shape as Tower's same-origin key injection into
|
|
83
|
+
* the dashboard shell: post-auth only, never cross-origin readable.
|
|
84
|
+
*
|
|
85
|
+
* Inserts `"connectionToken":"<token>"` as the first key of the HTML-escaped
|
|
86
|
+
* JSON in `data-settings` (no trailing comma for an empty `{}`). Returns null,
|
|
87
|
+
* so the caller relays the page unchanged, when the token is not
|
|
88
|
+
* `^[A-Za-z0-9-]+$`, the page carries no workbench configuration, the
|
|
89
|
+
* configuration does not parse, it already names a `connectionToken`, or the
|
|
90
|
+
* rewritten configuration would not decode to valid JSON carrying the token.
|
|
91
|
+
* Exported for testing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function injectWorkbenchConnectionToken(html: string, token: string): string | null;
|
|
48
94
|
/**
|
|
49
95
|
* Forward a plain HTTP request to the local IDE server. Assumes authorization
|
|
50
96
|
* has already passed. Path and query are forwarded unchanged (the workbench
|
|
@@ -59,4 +105,10 @@ export declare function forwardIdeHttp(req: http.IncomingMessage, res: http.Serv
|
|
|
59
105
|
* parse. Mirrors `tunnel-client.ts handleWebSocketConnect`'s pipe.
|
|
60
106
|
*/
|
|
61
107
|
export declare function forwardIdeWebSocket(req: http.IncomingMessage, socket: net.Socket, head: Buffer, log: (level: 'INFO' | 'ERROR' | 'WARN', message: string) => void): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Reconstruct the HTTP/1.1 101 switching-protocols response line + headers.
|
|
110
|
+
* With `stripTokenCookie` (a LOCAL request) it drops the IDE server's
|
|
111
|
+
* `vscode-tkn` Set-Cookie like the HTTP path does. Exported for testing.
|
|
112
|
+
*/
|
|
113
|
+
export declare function rebuildUpgradeResponse(res: Pick<http.IncomingMessage, 'statusCode' | 'statusMessage' | 'headers'>, stripTokenCookie: boolean): string;
|
|
62
114
|
//# sourceMappingURL=ide-forward.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ide-forward.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/ide-forward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"ide-forward.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/ide-forward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAKhC,OAAO,EAAsB,cAAc,EAA0B,MAAM,sBAAsB,CAAC;AAmDlG,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIzG;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAazF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,IAAI,CAa3D;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,IAAI,CAAC,mBAAmB,EAClC,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,eAAe,GAAG,IAAI,EACvC,eAAe,EAAE,MAAM,GAAG,IAAI,GAC7B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CA4CnC;AAED,6FAA6F;AAC7F,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAKxG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EACjC,gBAAgB,EAAE,OAAO,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,CAe5C;AAKD,8FAA8F;AAC9F,eAAO,MAAM,wBAAwB,QAAkB,CAAC;AA4BxD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBzF;AA0DD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAC/D,OAAO,CAAC,IAAI,CAAC,CA2Cf;AAaD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,MAAM,EAAE,GAAG,CAAC,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAC/D,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,YAAY,GAAG,eAAe,GAAG,SAAS,CAAC,EAC3E,gBAAgB,EAAE,OAAO,GACxB,MAAM,CAeR"}
|
|
@@ -17,8 +17,9 @@ import { URL } from 'node:url';
|
|
|
17
17
|
import { readCloudConfig } from '../lib/cloud-config.js';
|
|
18
18
|
import { TUNNEL_PROXY_HEADER } from '../lib/tunnel-client.js';
|
|
19
19
|
import { TOWER_KEY_HEADER, LEGACY_WEB_KEY_HEADER } from '@cluesmith/codev-types';
|
|
20
|
-
import { IDE_DEFAULT_PREFIX, readIdeConnectionToken } from '../lib/ide-record.js';
|
|
20
|
+
import { IDE_DEFAULT_PREFIX, matchIdePrefix, readIdeConnectionToken } from '../lib/ide-record.js';
|
|
21
21
|
import { getRecordedIdePort, ensureIdeServerLive } from './ide-server.js';
|
|
22
|
+
import { IDE_FORWARD_COOKIE } from '../utils/server-utils.js';
|
|
22
23
|
/** Hop-by-hop headers not forwarded between connections (RFC 7230 §6.1). */
|
|
23
24
|
const HOP_BY_HOP = new Set([
|
|
24
25
|
'connection',
|
|
@@ -38,16 +39,31 @@ const HOP_BY_HOP = new Set([
|
|
|
38
39
|
* server must never see it.
|
|
39
40
|
* - `x-codev-tunnel-proxy`: Tower's tunnel marker — never forwarded, so it
|
|
40
41
|
* cannot leak or be trusted downstream (marker integrity, #1674).
|
|
42
|
+
* - `x-forwarded-host` / `-proto` / `-prefix`: only Tower's own stamps reach the
|
|
43
|
+
* IDE server (Issue #1869); a client copy could steer the workbench's
|
|
44
|
+
* `remoteAuthority` or base path.
|
|
41
45
|
* - `host`: replaced with the loopback target authority below.
|
|
42
46
|
*/
|
|
43
47
|
const STRIP_BEFORE_FORWARD = new Set([
|
|
44
48
|
'x-forwarded-port',
|
|
45
49
|
'x-original-host',
|
|
50
|
+
// Tower's own x-forwarded-* stamps are the only ones the IDE server may see
|
|
51
|
+
// (Issue #1869): client-supplied copies are dropped on every path.
|
|
52
|
+
'x-forwarded-host',
|
|
53
|
+
'x-forwarded-proto',
|
|
54
|
+
'x-forwarded-prefix',
|
|
46
55
|
TOWER_KEY_HEADER,
|
|
47
56
|
LEGACY_WEB_KEY_HEADER,
|
|
48
57
|
TUNNEL_PROXY_HEADER,
|
|
49
58
|
'host',
|
|
50
59
|
]);
|
|
60
|
+
/**
|
|
61
|
+
* Response headers never relayed from the IDE server, on any path (Issue #1869):
|
|
62
|
+
* Tower must NEVER send `Access-Control-Allow-Credentials` (Tower reflects any
|
|
63
|
+
* loopback `Origin`, so with it another localhost page could read
|
|
64
|
+
* cookie-authorized `/ide` responses).
|
|
65
|
+
*/
|
|
66
|
+
const STRIP_FROM_RESPONSE = new Set(['access-control-allow-credentials']);
|
|
51
67
|
/** True if this request arrived over the tunnel (stamped by `TunnelClient`, #1588). */
|
|
52
68
|
function isTunnelBorne(headers) {
|
|
53
69
|
return headers[TUNNEL_PROXY_HEADER] === '1';
|
|
@@ -55,17 +71,38 @@ function isTunnelBorne(headers) {
|
|
|
55
71
|
/** The connection-token cookie name the VS Code server reads. */
|
|
56
72
|
const IDE_TOKEN_COOKIE = 'vscode-tkn';
|
|
57
73
|
/**
|
|
58
|
-
*
|
|
59
|
-
* preserving every
|
|
60
|
-
* `vscode-tkn` (Issue #
|
|
74
|
+
* Set the IDE connection token in a Cookie header value as `vscode-tkn=…`,
|
|
75
|
+
* preserving every other cookie. Tower's token REPLACES any incoming
|
|
76
|
+
* `vscode-tkn` (Issue #1869): Tower is the sole legitimate source, so a
|
|
77
|
+
* browser-held copy (a hand-built `?tkn=` URL, a pre-fix session, a token
|
|
78
|
+
* stale after rotation) is never authoritative. Exported for testing.
|
|
61
79
|
*/
|
|
62
80
|
export function mergeConnectionTokenCookie(existing, token) {
|
|
63
|
-
const
|
|
64
|
-
// Never clobber: if a vscode-tkn is already present, leave the header as-is.
|
|
65
|
-
if (new RegExp(`(^|;\\s*)${IDE_TOKEN_COOKIE}=`).test(current))
|
|
66
|
-
return current;
|
|
81
|
+
const others = stripCookie(existing, IDE_TOKEN_COOKIE);
|
|
67
82
|
const pair = `${IDE_TOKEN_COOKIE}=${token}`;
|
|
68
|
-
return
|
|
83
|
+
return others ? `${others}; ${pair}` : pair;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Remove one cookie (exact name) from a Cookie header value, preserving every
|
|
87
|
+
* other pair in order. Accepts the string | string[] shape. Returns '' when no
|
|
88
|
+
* pair remains. Used to keep Tower's forward cookie (Issue #1869) away from the
|
|
89
|
+
* IDE server, the way the key header is stripped. Exported for testing.
|
|
90
|
+
*/
|
|
91
|
+
export function stripCookie(existing, name) {
|
|
92
|
+
if (existing === undefined)
|
|
93
|
+
return '';
|
|
94
|
+
const joined = Array.isArray(existing) ? existing.join('; ') : existing;
|
|
95
|
+
return joined
|
|
96
|
+
.split(';')
|
|
97
|
+
.map((pair) => pair.trim())
|
|
98
|
+
.filter((pair) => {
|
|
99
|
+
if (pair.length === 0)
|
|
100
|
+
return false;
|
|
101
|
+
const eq = pair.indexOf('=');
|
|
102
|
+
const pairName = eq < 0 ? pair : pair.slice(0, eq).trim();
|
|
103
|
+
return pairName !== name;
|
|
104
|
+
})
|
|
105
|
+
.join('; ');
|
|
69
106
|
}
|
|
70
107
|
/**
|
|
71
108
|
* Derive the tunnel's public authority from the cloud config, matching the
|
|
@@ -111,38 +148,216 @@ export function buildForwardHeaders(incoming, port, publicAuthority, connectionT
|
|
|
111
148
|
out[key] = value;
|
|
112
149
|
}
|
|
113
150
|
out['host'] = `127.0.0.1:${port}`;
|
|
114
|
-
if (isTunnelBorne(incoming)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
151
|
+
if (isTunnelBorne(incoming)) {
|
|
152
|
+
if (publicAuthority) {
|
|
153
|
+
out['x-forwarded-host'] = publicAuthority.host;
|
|
154
|
+
out['x-forwarded-proto'] = publicAuthority.proto;
|
|
155
|
+
out['x-forwarded-prefix'] = publicAuthority.prefix;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (typeof incoming.host === 'string' && incoming.host.length > 0) {
|
|
159
|
+
// Local browser (Issue #1869): the IDE server builds the workbench's
|
|
160
|
+
// `remoteAuthority` from the Host it sees, and the rewrite above makes that
|
|
161
|
+
// its own port, so the workbench would dial the IDE server directly and
|
|
162
|
+
// bypass Tower's forward seam. Stamp Tower's own authority (the incoming
|
|
163
|
+
// Host, already Host-guarded) so the workbench's sockets and remote
|
|
164
|
+
// resources come back through Tower. No prefix: locally `/ide` IS the
|
|
165
|
+
// server's base path.
|
|
166
|
+
out['x-forwarded-host'] = incoming.host;
|
|
167
|
+
out['x-forwarded-proto'] = 'http';
|
|
118
168
|
}
|
|
119
169
|
// Present the IDE server's connection token as a cookie. The server is
|
|
120
|
-
// spawned with `--connection-token-file` and 403s without it, and
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
170
|
+
// spawned with `--connection-token-file` and 403s without it, and the
|
|
171
|
+
// browser never holds the token (Tower strips the server's Set-Cookie,
|
|
172
|
+
// Issue #1869), so Tower injects it here on every request (Issue #1668),
|
|
173
|
+
// REPLACING any browser-held `vscode-tkn` and keeping every other cookie.
|
|
174
|
+
// Tower's own forward cookie is stripped first: the IDE server must never
|
|
175
|
+
// see it (Issue #1869).
|
|
176
|
+
const cookie = stripCookie(incoming.cookie, IDE_FORWARD_COOKIE);
|
|
177
|
+
delete out['cookie'];
|
|
124
178
|
if (connectionToken) {
|
|
125
|
-
out['cookie'] = mergeConnectionTokenCookie(
|
|
179
|
+
out['cookie'] = mergeConnectionTokenCookie(cookie, connectionToken);
|
|
180
|
+
}
|
|
181
|
+
else if (cookie) {
|
|
182
|
+
out['cookie'] = cookie;
|
|
126
183
|
}
|
|
127
184
|
return out;
|
|
128
185
|
}
|
|
129
|
-
/** True if `pathname` targets the IDE forward prefix
|
|
130
|
-
export
|
|
131
|
-
|
|
132
|
-
|
|
186
|
+
/** True if `pathname` targets the IDE forward prefix; defined beside the prefix constant. */
|
|
187
|
+
export { matchIdePrefix };
|
|
188
|
+
/**
|
|
189
|
+
* Drop every `vscode-tkn` cookie the IDE server tries to set in the browser
|
|
190
|
+
* (Issue #1869). The server answers an authenticated request with
|
|
191
|
+
* `Set-Cookie: vscode-tkn=<connection token>` (7 days, SameSite=Lax); cookies
|
|
192
|
+
* are not port-scoped, so a browser holding it could open the remote protocol
|
|
193
|
+
* on the IDE server's own port from any localhost page, bypassing Tower. Tower
|
|
194
|
+
* injects the token on every forwarded request, so the browser never needs it.
|
|
195
|
+
* Returns undefined when nothing remains. Exported for testing.
|
|
196
|
+
*/
|
|
197
|
+
export function stripConnectionTokenSetCookie(value) {
|
|
198
|
+
if (value === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
201
|
+
const kept = entries.filter((entry) => !new RegExp(`^\\s*${IDE_TOKEN_COOKIE}=`).test(entry));
|
|
202
|
+
return kept.length > 0 ? kept : undefined;
|
|
133
203
|
}
|
|
134
|
-
/**
|
|
135
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Copy response headers, dropping hop-by-hop; preserves `Cache-Control` /
|
|
206
|
+
* `ETag`. With `stripTokenCookie` (a LOCAL request, Issue #1869) it also drops
|
|
207
|
+
* the IDE server's `vscode-tkn` Set-Cookie; the tunnel path relays it unchanged
|
|
208
|
+
* (the cloud variant is a separate follow-up). Exported for testing.
|
|
209
|
+
*/
|
|
210
|
+
export function copyResponseHeaders(headers, stripTokenCookie) {
|
|
136
211
|
const out = {};
|
|
137
212
|
for (const [key, value] of Object.entries(headers)) {
|
|
138
213
|
if (value === undefined)
|
|
139
214
|
continue;
|
|
140
|
-
|
|
215
|
+
const lower = key.toLowerCase();
|
|
216
|
+
if (HOP_BY_HOP.has(lower))
|
|
217
|
+
continue;
|
|
218
|
+
if (STRIP_FROM_RESPONSE.has(lower))
|
|
219
|
+
continue;
|
|
220
|
+
if (stripTokenCookie && lower === 'set-cookie') {
|
|
221
|
+
const kept = stripConnectionTokenSetCookie(value);
|
|
222
|
+
if (kept)
|
|
223
|
+
out[key] = kept;
|
|
141
224
|
continue;
|
|
225
|
+
}
|
|
142
226
|
out[key] = value;
|
|
143
227
|
}
|
|
144
228
|
return out;
|
|
145
229
|
}
|
|
230
|
+
/** Opening of the workbench configuration the IDE server embeds in its page. */
|
|
231
|
+
const WORKBENCH_CONFIG_OPEN = '<meta id="vscode-workbench-web-configuration" data-settings="{';
|
|
232
|
+
/** Largest workbench page Tower will buffer to rewrite; anything bigger streams unchanged. */
|
|
233
|
+
export const MAX_WORKBENCH_HTML_BYTES = 2 * 1024 * 1024;
|
|
234
|
+
/** Only a plain alphanumeric token is ever written into the page. */
|
|
235
|
+
const SAFE_TOKEN = /^[A-Za-z0-9-]+$/;
|
|
236
|
+
/** Decode the HTML character references the server uses in `data-settings`. */
|
|
237
|
+
function decodeAttribute(value) {
|
|
238
|
+
return value
|
|
239
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
240
|
+
.replace(/&#(\d+);/g, (_m, dec) => String.fromCodePoint(parseInt(dec, 10)))
|
|
241
|
+
.replace(/"/g, '"')
|
|
242
|
+
.replace(/'|'/g, "'")
|
|
243
|
+
.replace(/</g, '<')
|
|
244
|
+
.replace(/>/g, '>')
|
|
245
|
+
.replace(/&/g, '&');
|
|
246
|
+
}
|
|
247
|
+
/** The decoded `data-settings` JSON object, or null when it does not parse as one. */
|
|
248
|
+
function parseSettings(attribute) {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(decodeAttribute(attribute));
|
|
251
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
252
|
+
return null;
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Hand the workbench its connection token through its own configuration
|
|
261
|
+
* instead of the browser cookie jar (Issue #1869). The workbench takes
|
|
262
|
+
* `connectionToken` from its configuration first and falls back to the
|
|
263
|
+
* `vscode-tkn` cookie, which Tower strips locally because a cookie is readable
|
|
264
|
+
* by every localhost port. Same shape as Tower's same-origin key injection into
|
|
265
|
+
* the dashboard shell: post-auth only, never cross-origin readable.
|
|
266
|
+
*
|
|
267
|
+
* Inserts `"connectionToken":"<token>"` as the first key of the HTML-escaped
|
|
268
|
+
* JSON in `data-settings` (no trailing comma for an empty `{}`). Returns null,
|
|
269
|
+
* so the caller relays the page unchanged, when the token is not
|
|
270
|
+
* `^[A-Za-z0-9-]+$`, the page carries no workbench configuration, the
|
|
271
|
+
* configuration does not parse, it already names a `connectionToken`, or the
|
|
272
|
+
* rewritten configuration would not decode to valid JSON carrying the token.
|
|
273
|
+
* Exported for testing.
|
|
274
|
+
*/
|
|
275
|
+
export function injectWorkbenchConnectionToken(html, token) {
|
|
276
|
+
if (!SAFE_TOKEN.test(token))
|
|
277
|
+
return null;
|
|
278
|
+
const start = html.indexOf(WORKBENCH_CONFIG_OPEN);
|
|
279
|
+
if (start < 0)
|
|
280
|
+
return null;
|
|
281
|
+
const valueStart = start + WORKBENCH_CONFIG_OPEN.length - 1; // at the "{"
|
|
282
|
+
const valueEnd = html.indexOf('"', valueStart);
|
|
283
|
+
if (valueEnd < 0)
|
|
284
|
+
return null;
|
|
285
|
+
const original = html.slice(valueStart, valueEnd);
|
|
286
|
+
const settings = parseSettings(original);
|
|
287
|
+
if (!settings || 'connectionToken' in settings)
|
|
288
|
+
return null;
|
|
289
|
+
const isEmpty = Object.keys(settings).length === 0;
|
|
290
|
+
const entry = `"connectionToken":"${token}"`;
|
|
291
|
+
let rewritten = `{${entry},${original.slice(1)}`;
|
|
292
|
+
if (isEmpty)
|
|
293
|
+
rewritten = `{${entry}}`;
|
|
294
|
+
const check = parseSettings(rewritten);
|
|
295
|
+
if (!check || check.connectionToken !== token)
|
|
296
|
+
return null;
|
|
297
|
+
return html.slice(0, valueStart) + rewritten + html.slice(valueEnd);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* True for a response Tower may rewrite: 200, uncompressed `text/html`, and not
|
|
301
|
+
* declared larger than the bound. Everything else streams unchanged.
|
|
302
|
+
*/
|
|
303
|
+
function isRewritableWorkbenchPage(res) {
|
|
304
|
+
if (res.statusCode !== 200)
|
|
305
|
+
return false;
|
|
306
|
+
const type = res.headers['content-type'] ?? '';
|
|
307
|
+
if (!type.startsWith('text/html'))
|
|
308
|
+
return false;
|
|
309
|
+
if (res.headers['content-encoding'] !== undefined)
|
|
310
|
+
return false;
|
|
311
|
+
const declared = Number(res.headers['content-length']);
|
|
312
|
+
if (Number.isFinite(declared) && declared > MAX_WORKBENCH_HTML_BYTES)
|
|
313
|
+
return false;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Buffer a workbench page (up to the bound), inject the connection token, and
|
|
318
|
+
* relay it with a corrected Content-Length. A body that outgrows the bound
|
|
319
|
+
* mid-stream is relayed unchanged: the buffered bytes, then a pipe of the rest.
|
|
320
|
+
*/
|
|
321
|
+
function relayWorkbenchHtml(proxyRes, res, token) {
|
|
322
|
+
const chunks = [];
|
|
323
|
+
let size = 0;
|
|
324
|
+
let streaming = false;
|
|
325
|
+
const onData = (chunk) => {
|
|
326
|
+
chunks.push(chunk);
|
|
327
|
+
size += chunk.length;
|
|
328
|
+
if (size <= MAX_WORKBENCH_HTML_BYTES)
|
|
329
|
+
return;
|
|
330
|
+
streaming = true;
|
|
331
|
+
proxyRes.off('data', onData);
|
|
332
|
+
res.writeHead(proxyRes.statusCode ?? 502, copyResponseHeaders(proxyRes.headers, true));
|
|
333
|
+
for (const buffered of chunks)
|
|
334
|
+
res.write(buffered);
|
|
335
|
+
proxyRes.pipe(res);
|
|
336
|
+
};
|
|
337
|
+
proxyRes.on('data', onData);
|
|
338
|
+
proxyRes.on('end', () => {
|
|
339
|
+
if (streaming)
|
|
340
|
+
return;
|
|
341
|
+
const original = Buffer.concat(chunks).toString('utf8');
|
|
342
|
+
const rewritten = injectWorkbenchConnectionToken(original, token);
|
|
343
|
+
const body = Buffer.from(rewritten ?? original, 'utf8');
|
|
344
|
+
const out = copyResponseHeaders(proxyRes.headers, true);
|
|
345
|
+
for (const key of Object.keys(out)) {
|
|
346
|
+
const lower = key.toLowerCase();
|
|
347
|
+
if (lower === 'content-length' || lower === 'transfer-encoding')
|
|
348
|
+
delete out[key];
|
|
349
|
+
}
|
|
350
|
+
out['content-length'] = body.length;
|
|
351
|
+
res.writeHead(proxyRes.statusCode ?? 502, out);
|
|
352
|
+
res.end(body);
|
|
353
|
+
});
|
|
354
|
+
proxyRes.on('error', () => {
|
|
355
|
+
if (!res.headersSent)
|
|
356
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
357
|
+
if (!res.writableEnded)
|
|
358
|
+
res.end();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
146
361
|
/**
|
|
147
362
|
* Forward a plain HTTP request to the local IDE server. Assumes authorization
|
|
148
363
|
* has already passed. Path and query are forwarded unchanged (the workbench
|
|
@@ -158,9 +373,17 @@ export async function forwardIdeHttp(req, res, log) {
|
|
|
158
373
|
res.end(JSON.stringify({ error: 'IDE server unavailable' }));
|
|
159
374
|
return;
|
|
160
375
|
}
|
|
161
|
-
const
|
|
376
|
+
const connectionToken = readIdeConnectionToken();
|
|
377
|
+
const local = !isTunnelBorne(req.headers);
|
|
378
|
+
const headers = buildForwardHeaders(req.headers, port, getPublicAuthority(), connectionToken);
|
|
162
379
|
const proxyReq = http.request({ hostname: '127.0.0.1', port, path: req.url || '/', method: req.method, headers }, (proxyRes) => {
|
|
163
|
-
|
|
380
|
+
// Local only (Issue #1869): the browser gets the token through the
|
|
381
|
+
// workbench configuration, never as a cookie. The tunnel path is unchanged.
|
|
382
|
+
if (local && connectionToken && isRewritableWorkbenchPage(proxyRes)) {
|
|
383
|
+
relayWorkbenchHtml(proxyRes, res, connectionToken);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
res.writeHead(proxyRes.statusCode ?? 502, copyResponseHeaders(proxyRes.headers, local));
|
|
164
387
|
proxyRes.pipe(res);
|
|
165
388
|
proxyRes.on('error', () => {
|
|
166
389
|
if (!res.writableEnded)
|
|
@@ -221,7 +444,7 @@ export async function forwardIdeWebSocket(req, socket, head, log) {
|
|
|
221
444
|
});
|
|
222
445
|
upstream.on('upgrade', (_upstreamRes, upstreamSocket, upstreamHead) => {
|
|
223
446
|
// Replay the handshake response and any buffered bytes, then pipe both ways.
|
|
224
|
-
socket.write(rebuildUpgradeResponse(_upstreamRes));
|
|
447
|
+
socket.write(rebuildUpgradeResponse(_upstreamRes, !isTunnelBorne(req.headers)));
|
|
225
448
|
if (upstreamHead.length > 0)
|
|
226
449
|
socket.write(upstreamHead);
|
|
227
450
|
if (head.length > 0)
|
|
@@ -254,11 +477,20 @@ export async function forwardIdeWebSocket(req, socket, head, log) {
|
|
|
254
477
|
});
|
|
255
478
|
upstream.end();
|
|
256
479
|
}
|
|
257
|
-
/**
|
|
258
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Reconstruct the HTTP/1.1 101 switching-protocols response line + headers.
|
|
482
|
+
* With `stripTokenCookie` (a LOCAL request) it drops the IDE server's
|
|
483
|
+
* `vscode-tkn` Set-Cookie like the HTTP path does. Exported for testing.
|
|
484
|
+
*/
|
|
485
|
+
export function rebuildUpgradeResponse(res, stripTokenCookie) {
|
|
259
486
|
const statusLine = `HTTP/1.1 ${res.statusCode ?? 101} ${res.statusMessage ?? 'Switching Protocols'}`;
|
|
260
487
|
const lines = [statusLine];
|
|
261
|
-
for (const [key,
|
|
488
|
+
for (const [key, raw] of Object.entries(res.headers)) {
|
|
489
|
+
if (STRIP_FROM_RESPONSE.has(key.toLowerCase()))
|
|
490
|
+
continue;
|
|
491
|
+
let value = raw;
|
|
492
|
+
if (stripTokenCookie && key.toLowerCase() === 'set-cookie')
|
|
493
|
+
value = stripConnectionTokenSetCookie(raw);
|
|
262
494
|
if (value === undefined)
|
|
263
495
|
continue;
|
|
264
496
|
if (Array.isArray(value)) {
|