Skip to content

Commit 2a3c835

Browse files
authored
Make webviews load locally (#16)
* Make webviews load locally Instead of using Microsoft's hosted endpoint. This also fixes issues when using code-server over http since some browsers refuse to load http content (like when using localhost) in https contexts (since the iframe was always https). * Revert path checks in web server We cannot use `includes` because this causes problems with endpoints that may share portions of their paths. For example /webview/uuid/service-worker.js was colliding with the /service-worker.js but moving it before caused static assets that load from a directory called /webview to start failing. We could move static up as well but this may just cause other conflicts. The standard ways of handling base paths seem to be to either ask the user for the base path or require that a rewriting proxy be put in front of the application (which rewrites /base/path/my/path to /my/path before it ever gets to the application). code-server currently requires the latter and does not support the former but either way the code will look similar (in the former case we would just strip the base path first, maybe before calling `handle`). For reference, the proxy in the Coder product is a rewriting proxy. This also fixes issues when the web server is behind multiple Express endpoints. With the existing code /anything/works/ rather than just the endpoints you specify (/vscode and / in our case) which is unexpected. It also makes it possible to browse without a trailing slash, for example /vscode, although that is currently broken for other reasons.
1 parent e232be7 commit 2a3c835

File tree

3 files changed

+82
-30
lines changed

3 files changed

+82
-30
lines changed

src/vs/code/browser/workbench/workbench.ts

+19
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,25 @@ class WindowIndicator implements IWindowIndicator {
569569
// Finally create workbench
570570
create(document.body, {
571571
...config,
572+
/**
573+
* Override relative URLs in the product configuration against the window
574+
* location as necessary. Only paths that must be absolute need to be
575+
* rewritten (for example the webview endpoint); the rest can be left
576+
* relative (for example the update path).
577+
*
578+
* @author coder
579+
*/
580+
productConfiguration: {
581+
...config.productConfiguration,
582+
// The webview endpoint contains variables in the format {{var}} so decode
583+
// them as `new URI` will encode them.
584+
webviewContentExternalBaseUrlTemplate: decodeURIComponent(
585+
new URL(
586+
config.productConfiguration?.webviewContentExternalBaseUrlTemplate ?? "",
587+
window.location.toString(), // This works without toString() but TypeScript thinks otherwise.
588+
).toString(),
589+
),
590+
},
572591
developmentOptions: {
573592
logLevel: logLevel ? parseLogLevel(logLevel) : undefined,
574593
...config.developmentOptions

src/vs/server/webClientServer.ts

+58-27
Original file line numberDiff line numberDiff line change
@@ -85,51 +85,59 @@ export class WebClientServer {
8585
private readonly _productService: IProductService
8686
) { }
8787

88-
/**
89-
* @coder Patched to handle an arbitrary path depth, such as in Coder Enterprise.
90-
*/
9188
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
9289
try {
9390
const pathname = parsedUrl.pathname!;
94-
const parsedPath = path.parse(pathname);
95-
const pathPrefix = getPathPrefix(pathname);
9691

97-
if (['manifest.json', 'webmanifest.json'].includes(parsedPath.base)) {
92+
/**
93+
* Add a custom manifest.
94+
*
95+
* @author coder
96+
*/
97+
if (pathname === '/manifest.json' || pathname === '/webmanifest.json') {
9898
return this._handleManifest(req, res, parsedUrl);
9999
}
100-
101-
if (['favicon.ico', 'code-192.png', 'code-512.png'].includes(parsedPath.base)) {
100+
if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname === '/code-192.png' || pathname === '/code-512.png') {
102101
// always serve icons/manifest, even without a token
103-
return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', parsedPath.base));
102+
return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', pathname.substr(1)));
104103
}
105-
106-
if (parsedPath.base === this._environmentService.serviceWorkerFileName) {
107-
return serveFile(this._logService, req, res, this._environmentService.serviceWorkerPath, {
108-
'Service-Worker-Allowed': pathPrefix,
109-
});
104+
/**
105+
* Add an endpoint for self-hosting webviews. This must be unique per
106+
* webview as the code relies on unique service workers. In our case we
107+
* use /webview/{{uuid}}.
108+
*
109+
* @author coder
110+
*/
111+
if (/^\/webview\//.test(pathname)) {
112+
// always serve webview requests, even without a token
113+
return this._handleWebview(req, res, parsedUrl);
110114
}
111-
112-
if (parsedPath.dir.includes('/static/') && parsedPath.ext) {
113-
parsedUrl.pathname = pathname.substring(pathname.indexOf('/static/'));
115+
if (/^\/static\//.test(pathname)) {
114116
// always serve static requests, even without a token
115117
return this._handleStatic(req, res, parsedUrl);
116118
}
117-
118-
if (parsedPath.base === 'callback') {
119+
/**
120+
* Move the service worker to the root. This makes the scope the root
121+
* (otherwise we would need to include Service-Worker-Allowed).
122+
*
123+
* @author coder
124+
*/
125+
if (pathname === '/' + this._environmentService.serviceWorkerFileName) {
126+
return serveFile(this._logService, req, res, this._environmentService.serviceWorkerPath);
127+
}
128+
if (pathname === '/') {
129+
// the token handling is done inside the handler
130+
return this._handleRoot(req, res, parsedUrl);
131+
}
132+
if (pathname === '/callback') {
119133
// callback support
120134
return this._handleCallback(req, res, parsedUrl);
121135
}
122-
123-
if (parsedPath.base === 'fetch-callback') {
136+
if (pathname === '/fetch-callback') {
124137
// callback fetch support
125138
return this._handleFetchCallback(req, res, parsedUrl);
126139
}
127140

128-
if (pathname.endsWith('/')) {
129-
// the token handling is done inside the handler
130-
return this._handleRoot(req, res, parsedUrl);
131-
}
132-
133141
const message = `"${parsedUrl.pathname}" not found.`;
134142
const error = new Error(message);
135143
req.emit('error', error);
@@ -240,6 +248,28 @@ export class WebClientServer {
240248
return serveFile(this._logService, req, res, filePath, headers);
241249
}
242250

251+
/**
252+
* Handle HTTP requests for /webview/*
253+
*
254+
* A unique path is required for every webview service worker.
255+
*/
256+
private async _handleWebview(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
257+
const headers: Record<string, string> = Object.create(null);
258+
259+
// support paths that are uri-encoded (e.g. spaces => %20)
260+
const normalizedPathname = decodeURIComponent(parsedUrl.pathname!);
261+
262+
// Strip `/webview/{uuid}` from the path.
263+
const relativeFilePath = normalize(normalizedPathname.split('/').splice(3).join('/'));
264+
265+
const filePath = join(APP_ROOT, 'out/vs/workbench/contrib/webview/browser/pre', relativeFilePath);
266+
if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) {
267+
return this.serveError(req, res, 400, `Bad request.`, parsedUrl);
268+
}
269+
270+
return serveFile(this._logService, req, res, filePath, headers);
271+
}
272+
243273
/**
244274
* Handle HTTP requests for /
245275
*/
@@ -318,6 +348,7 @@ export class WebClientServer {
318348
logoutEndpointUrl: this.createRequestUrl(req, parsedUrl, '/logout').toString(),
319349
webEndpointUrl: this.createRequestUrl(req, parsedUrl, '/static').toString(),
320350
webEndpointUrlTemplate: this.createRequestUrl(req, parsedUrl, '/static').toString(),
351+
webviewContentExternalBaseUrlTemplate: './webview/{{uuid}}/',
321352

322353
updateUrl: './update/check'
323354
},
@@ -343,7 +374,7 @@ export class WebClientServer {
343374
// the sha is the same as in src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html
344375
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-cb2sg39EJV8ABaSNFfWu/ou8o1xVXYK7jp90oZ9vpcg=';`,
345376
'child-src \'self\';',
346-
`frame-src 'self' https://*.vscode-webview.net ${this._productService.webEndpointUrl || ''} data:;`,
377+
`frame-src 'self' ${this._productService.webEndpointUrl || ''} data:;`,
347378
'worker-src \'self\' data:;',
348379
'style-src \'self\' \'unsafe-inline\';',
349380
'connect-src \'self\' ws: wss: https:;',

src/vs/workbench/browser/web.main.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,15 @@ class BrowserMain extends Disposable {
102102
// Startup
103103
const instantiationService = workbench.startup();
104104

105-
/** @coder Initialize our own client-side additions. */
105+
/**
106+
* Initialize our own client-side additions.
107+
*
108+
* @author Coder
109+
*/
106110
if (!this.configuration.productConfiguration) {
107111
throw new Error('`productConfiguration` not present in workbench config');
108112
}
109-
110113
const codeServerClientAdditions = this._register(instantiationService.createInstance(CodeServerClientAdditions, this.configuration.productConfiguration));
111-
112114
await codeServerClientAdditions.startup();
113115

114116
// Window

0 commit comments

Comments
 (0)