Skip to content

Commit 5b2f96e

Browse files
committed
Add support for using the test resolver in the web
1 parent 0876c19 commit 5b2f96e

File tree

6 files changed

+171
-5
lines changed

6 files changed

+171
-5
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*---------------------------------------------------------------------------------------------
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+
//@ts-check
7+
8+
'use strict';
9+
10+
const withBrowserDefaults = require('../shared.webpack.config').browser;
11+
12+
module.exports = withBrowserDefaults({
13+
context: __dirname,
14+
entry: {
15+
extension: './src/extension.browser.ts'
16+
},
17+
output: {
18+
filename: 'testResolverMain.js'
19+
}
20+
});

extensions/vscode-test-resolver/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
"version": "0.0.1",
55
"publisher": "vscode",
66
"license": "MIT",
7-
"enableProposedApi": true,
87
"enabledApiProposals": [
98
"resolvers",
10-
"tunnels"
9+
"tunnels"
1110
],
1211
"private": true,
1312
"engines": {
@@ -32,6 +31,7 @@
3231
"onCommand:vscode-testresolver.toggleConnectionPause"
3332
],
3433
"main": "./out/extension",
34+
"browser": "./dist/browser/testResolverMain",
3535
"devDependencies": {
3636
"@types/node": "16.x"
3737
},
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*---------------------------------------------------------------------------------------------
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 vscode from 'vscode';
7+
8+
export function activate(_context: vscode.ExtensionContext) {
9+
vscode.workspace.registerRemoteAuthorityResolver('test', {
10+
async resolve(_authority: string): Promise<vscode.ResolverResult> {
11+
console.log(`Resolving ${_authority}`);
12+
return new vscode.ManagedResolvedAuthority(async () => {
13+
return new InitialManagedMessagePassing();
14+
});
15+
}
16+
});
17+
}
18+
19+
/**
20+
* The initial message passing is a bit special because we need to
21+
* wait for the HTTP headers to arrive before we can create the
22+
* actual WebSocket.
23+
*/
24+
class InitialManagedMessagePassing implements vscode.ManagedMessagePassing {
25+
private readonly dataEmitter = new vscode.EventEmitter<Uint8Array>();
26+
private readonly closeEmitter = new vscode.EventEmitter<Error | undefined>();
27+
private readonly endEmitter = new vscode.EventEmitter<void>();
28+
29+
public readonly onDidReceiveMessage = this.dataEmitter.event;
30+
public readonly onDidClose = this.closeEmitter.event;
31+
public readonly onDidEnd = this.endEmitter.event;
32+
33+
private _actual: OpeningManagedMessagePassing | null = null;
34+
private _isDisposed = false;
35+
36+
public send(d: Uint8Array): void {
37+
if (this._actual) {
38+
// we already got the HTTP headers
39+
this._actual.send(d);
40+
return;
41+
}
42+
43+
if (this._isDisposed) {
44+
// got disposed in the meantime, ignore
45+
return;
46+
}
47+
48+
// we now received the HTTP headers
49+
const decoder = new TextDecoder();
50+
const str = decoder.decode(d);
51+
52+
// example str GET ws://localhost/oss-dev?reconnectionToken=4354a323-a45a-452c-b5d7-d8d586e1cd5c&reconnection=false&skipWebSocketFrames=true HTTP/1.1
53+
const match = str.match(/GET\s+(\S+)\s+HTTP/);
54+
if (!match) {
55+
console.error(`Coult not parse ${str}`);
56+
this.closeEmitter.fire(new Error(`Coult not parse ${str}`));
57+
return;
58+
}
59+
60+
// example url ws://localhost/oss-dev?reconnectionToken=4354a323-a45a-452c-b5d7-d8d586e1cd5c&reconnection=false&skipWebSocketFrames=true
61+
const url = new URL(match[1]);
62+
63+
// extract path and query from url using browser's URL
64+
const parsedUrl = new URL(url);
65+
this._actual = new OpeningManagedMessagePassing(parsedUrl, this.dataEmitter, this.closeEmitter, this.endEmitter);
66+
}
67+
68+
public end(): void {
69+
if (this._actual) {
70+
this._actual.end();
71+
return;
72+
}
73+
this._isDisposed = true;
74+
}
75+
}
76+
77+
class OpeningManagedMessagePassing {
78+
79+
private readonly socket: WebSocket;
80+
private isOpen = false;
81+
private bufferedData: Uint8Array[] = [];
82+
83+
constructor(
84+
url: URL,
85+
dataEmitter: vscode.EventEmitter<Uint8Array>,
86+
closeEmitter: vscode.EventEmitter<Error | undefined>,
87+
_endEmitter: vscode.EventEmitter<void>
88+
) {
89+
this.socket = new WebSocket(`ws://localhost:9888${url.pathname}${url.search.replace(/skipWebSocketFrames=true/, 'skipWebSocketFrames=false')}`);
90+
this.socket.addEventListener('close', () => closeEmitter.fire(undefined));
91+
this.socket.addEventListener('error', (e) => closeEmitter.fire(new Error(String(e))));
92+
this.socket.addEventListener('message', async (e) => {
93+
const arrayBuffer = await e.data.arrayBuffer();
94+
dataEmitter.fire(new Uint8Array(arrayBuffer));
95+
});
96+
this.socket.addEventListener('open', () => {
97+
while (this.bufferedData.length > 0) {
98+
const first = this.bufferedData.shift()!;
99+
this.socket.send(first);
100+
}
101+
this.isOpen = true;
102+
103+
// https://tools.ietf.org/html/rfc6455#section-4
104+
// const requestNonce = req.headers['sec-websocket-key'];
105+
// const hash = crypto.createHash('sha1');
106+
// hash.update(requestNonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
107+
// const responseNonce = hash.digest('base64');
108+
const responseHeaders = [
109+
`HTTP/1.1 101 Switching Protocols`,
110+
`Upgrade: websocket`,
111+
`Connection: Upgrade`,
112+
`Sec-WebSocket-Accept: TODO`
113+
];
114+
const textEncoder = new TextEncoder();
115+
textEncoder.encode(responseHeaders.join('\r\n') + '\r\n\r\n');
116+
dataEmitter.fire(textEncoder.encode(responseHeaders.join('\r\n') + '\r\n\r\n'));
117+
});
118+
}
119+
120+
public send(d: Uint8Array): void {
121+
if (!this.isOpen) {
122+
this.bufferedData.push(d);
123+
return;
124+
}
125+
this.socket.send(d);
126+
}
127+
128+
public end(): void {
129+
this.socket.close();
130+
}
131+
}

extensions/vscode-test-resolver/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"outDir": "./out",
55
"types": [
66
"node"
7+
],
8+
"lib": [
9+
"WebWorker"
710
]
811
},
912
"include": [

src/vs/server/node/serverEnvironmentService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const serverOptions: OptionDescriptions<Required<ServerParsedArgs>> = {
4848

4949
'enable-sync': { type: 'boolean' },
5050
'github-auth': { type: 'string' },
51+
'use-test-resolver': { type: 'boolean' },
5152

5253
/* ----- extension management ----- */
5354

@@ -165,6 +166,7 @@ export interface ServerParsedArgs {
165166

166167
'enable-sync'?: boolean;
167168
'github-auth'?: string;
169+
'use-test-resolver'?: boolean;
168170

169171
/* ----- extension management ----- */
170172

src/vs/server/node/webClientServer.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { isLinux } from 'vs/base/common/platform';
1515
import { ILogService } from 'vs/platform/log/common/log';
1616
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
1717
import { extname, dirname, join, normalize } from 'vs/base/common/path';
18-
import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas } from 'vs/base/common/network';
18+
import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas, builtinExtensionsPath } from 'vs/base/common/network';
1919
import { generateUuid } from 'vs/base/common/uuid';
2020
import { IProductService } from 'vs/platform/product/common/productService';
2121
import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken';
@@ -272,7 +272,12 @@ export class WebClientServer {
272272
return Array.isArray(val) ? val[0] : val;
273273
};
274274

275-
const remoteAuthority = getFirstHeader('x-original-host') || getFirstHeader('x-forwarded-host') || req.headers.host;
275+
const useTestResolver = (!this._environmentService.isBuilt && this._environmentService.args['use-test-resolver']);
276+
const remoteAuthority = (
277+
useTestResolver
278+
? 'test+test'
279+
: (getFirstHeader('x-original-host') || getFirstHeader('x-forwarded-host') || req.headers.host)
280+
);
276281
if (!remoteAuthority) {
277282
return serveError(req, res, 400, `Bad request.`);
278283
}
@@ -337,6 +342,11 @@ export class WebClientServer {
337342
WORKBENCH_NLS_BASE_URL: nlsBaseUrl ? `${nlsBaseUrl}${!nlsBaseUrl.endsWith('/') ? '/' : ''}${this._productService.commit}/${this._productService.version}/` : '',
338343
};
339344

345+
if (useTestResolver) {
346+
const filePath = FileAccess.asFileUri(`${builtinExtensionsPath}/vscode-test-resolver/package.json`).fsPath;
347+
const packageJSON = JSON.parse((await fsp.readFile(filePath)).toString());
348+
values['WORKBENCH_BUILTIN_EXTENSIONS'] = asJSON([{ extensionPath: 'vscode-test-resolver', packageJSON }]);
349+
}
340350

341351
let data;
342352
try {
@@ -351,7 +361,7 @@ export class WebClientServer {
351361
'default-src \'self\';',
352362
'img-src \'self\' https: data: blob:;',
353363
'media-src \'self\';',
354-
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
364+
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' ${useTestResolver ? '' : `http://${remoteAuthority}`};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html
355365
'child-src \'self\';',
356366
`frame-src 'self' https://*.vscode-cdn.net data:;`,
357367
'worker-src \'self\' data: blob:;',

0 commit comments

Comments
 (0)