mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-28 13:24:24 +08:00
fix: allow workbench origin for remote resources (#336549)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { app, BrowserWindow, desktopCapturer, Details, globalShortcut, GPUFeatureStatus, net, powerMonitor, protocol, screen as electronScreen, session, Session, systemPreferences, WebFrameMain } from 'electron';
|
||||
import { app, BrowserWindow, desktopCapturer, Details, globalShortcut, GPUFeatureStatus, powerMonitor, protocol, screen as electronScreen, session, Session, systemPreferences, WebFrameMain } from 'electron';
|
||||
import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js';
|
||||
import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js';
|
||||
import { hostname, release } from 'os';
|
||||
@@ -151,6 +151,7 @@ import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../pla
|
||||
import { ITerminalSandboxService, NullTerminalSandboxService } from '../../platform/sandbox/common/terminalSandboxService.js';
|
||||
import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js';
|
||||
import { IProtocolMainService } from '../../platform/protocol/electron-main/protocol.js';
|
||||
import { createRemoteResourceRequestHandler } from '../../platform/protocol/electron-main/remoteResourceProtocol.js';
|
||||
|
||||
type OSProxyConfigEvent = {
|
||||
readonly success: boolean;
|
||||
@@ -1634,22 +1635,7 @@ export class CodeApplication extends Disposable {
|
||||
this.installMutex();
|
||||
|
||||
// Remote Authorities
|
||||
protocol.handle(Schemas.vscodeRemoteResource, async request => {
|
||||
try {
|
||||
return await net.fetch(
|
||||
request.url.replace(/^vscode-remote-resource:/, 'http:'),
|
||||
{
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
bypassCustomProtocolHandlers: true
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logService.warn('error loading remote resource', error);
|
||||
return Response.error();
|
||||
}
|
||||
});
|
||||
protocol.handle(Schemas.vscodeRemoteResource, createRemoteResourceRequestHandler(this.logService));
|
||||
this._register(toDisposable(() => protocol.unhandle(Schemas.vscodeRemoteResource)));
|
||||
|
||||
// Start to fetch shell environment (if needed) after window has opened
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { net } from 'electron';
|
||||
import { Schemas } from '../../../base/common/network.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
|
||||
type RemoteResourceRequestHandler = (request: GlobalRequest) => Promise<GlobalResponse>;
|
||||
|
||||
export function createRemoteResourceRequestHandler(logService: ILogService): RemoteResourceRequestHandler {
|
||||
return async request => {
|
||||
try {
|
||||
return await net.fetch(
|
||||
request.url.replace(`${Schemas.vscodeRemoteResource}:`, `${Schemas.http}:`),
|
||||
{
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
bypassCustomProtocolHandlers: true
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logService.warn('error loading remote resource', error);
|
||||
return Response.error();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Schemas } from '../../../../base/common/network.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
|
||||
@@ -83,4 +84,17 @@ suite('ProtocolMainService', () => {
|
||||
requestCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('loads vscode-remote-resource with workbench CORS headers', async function () {
|
||||
this.timeout(15_000);
|
||||
const response = await ipcRenderer.invoke('vscode:test-remote-resource');
|
||||
|
||||
assert.deepStrictEqual(response, {
|
||||
loaded: true,
|
||||
requestHeaders: {
|
||||
origin: 'vscode-file://vscode-app',
|
||||
secFetchMode: 'cors',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ManagementConnection } from './remoteExtensionManagement.js';
|
||||
import { determineServerConnectionToken, requestHasValidConnectionToken as httpRequestHasValidConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from './serverConnectionToken.js';
|
||||
import { IServerEnvironmentService, ServerParsedArgs } from './serverEnvironmentService.js';
|
||||
import { IServerLifetimeService } from './serverLifetimeService.js';
|
||||
import { getRemoteResourceResponseHeaders } from './remoteResourceResponse.js';
|
||||
import { setupServerServices, SocketServer } from './serverServices.js';
|
||||
import { CacheControl, serveError, serveFile, WebClientServer } from './webClientServer.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -51,13 +52,6 @@ function parseRequestUrl(requestUrl: string): URL | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function getRemoteResourceResponseHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Content-Security-Policy': `default-src 'none'; sandbox`,
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
};
|
||||
}
|
||||
|
||||
declare namespace vsda {
|
||||
// the signer is a native module that for historical reasons uses a lower case class name
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -181,7 +175,8 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
|
||||
return serveError(req, res, 400, `Bad request.`);
|
||||
}
|
||||
|
||||
const responseHeaders = getRemoteResourceResponseHeaders();
|
||||
const requestOrigin = req.headers['origin'];
|
||||
const responseHeaders = getRemoteResourceResponseHeaders(requestOrigin, origin => this._webEndpointOriginChecker.matches(origin));
|
||||
if (this._environmentService.isBuilt) {
|
||||
if (isEqualOrParent(filePath, this._environmentService.builtinExtensionsPath, !platform.isLinux)
|
||||
|| isEqualOrParent(filePath, this._environmentService.extensionsPath, !platform.isLinux)
|
||||
@@ -190,12 +185,6 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow cross origin requests from the web worker extension host
|
||||
responseHeaders['Vary'] = 'Origin';
|
||||
const requestOrigin = req.headers['origin'];
|
||||
if (requestOrigin && this._webEndpointOriginChecker.matches(requestOrigin)) {
|
||||
responseHeaders['Access-Control-Allow-Origin'] = requestOrigin;
|
||||
}
|
||||
return serveFile(filePath, CacheControl.ETAG, this._logService, req, res, responseHeaders);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Schemas, VSCODE_AUTHORITY } from '../../base/common/network.js';
|
||||
|
||||
const NATIVE_WORKBENCH_ORIGIN = `${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}`;
|
||||
|
||||
export function getRemoteResourceResponseHeaders(requestOrigin: string | undefined, isAllowedWebEndpointOrigin: (origin: string) => boolean): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Security-Policy': `default-src 'none'; sandbox`,
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Vary': 'Origin',
|
||||
};
|
||||
|
||||
if (requestOrigin && (requestOrigin === NATIVE_WORKBENCH_ORIGIN || isAllowedWebEndpointOrigin(requestOrigin))) {
|
||||
headers['Access-Control-Allow-Origin'] = requestOrigin;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';
|
||||
import { getRemoteResourceResponseHeaders } from '../../node/remoteResourceResponse.js';
|
||||
|
||||
suite('Remote resource response', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('allows trusted workbench and configured web endpoint origins', () => {
|
||||
const allowedWebEndpointOrigin = 'https://example.vscode-cdn.net';
|
||||
const getHeaders = (origin: string | undefined) => getRemoteResourceResponseHeaders(origin, candidate => candidate === allowedWebEndpointOrigin);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
nativeWorkbench: getHeaders('vscode-file://vscode-app')['Access-Control-Allow-Origin'],
|
||||
webEndpoint: getHeaders(allowedWebEndpointOrigin)['Access-Control-Allow-Origin'],
|
||||
webview: getHeaders('vscode-webview://01234567-89ab-cdef-0123-456789abcdef')['Access-Control-Allow-Origin'],
|
||||
untrusted: getHeaders('https://example.com')['Access-Control-Allow-Origin'],
|
||||
vary: getHeaders(undefined).Vary,
|
||||
}, {
|
||||
nativeWorkbench: 'vscode-file://vscode-app',
|
||||
webEndpoint: allowedWebEndpointOrigin,
|
||||
webview: undefined,
|
||||
untrusted: undefined,
|
||||
vary: 'Origin',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Remote Resource Test</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -10,10 +10,11 @@
|
||||
// come before any mocha imports.
|
||||
process.env.MOCHA_COLORS = '1';
|
||||
|
||||
const { app, BrowserWindow, ipcMain, crashReporter, net: electronNet, session } = require('electron');
|
||||
const { app, BrowserWindow, ipcMain, crashReporter, net: electronNet, protocol, session } = require('electron');
|
||||
const product = require('../../../product.json');
|
||||
const { tmpdir } = require('os');
|
||||
const { existsSync, mkdirSync, promises } = require('fs');
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
const mocha = require('mocha');
|
||||
const events = require('events');
|
||||
@@ -25,6 +26,17 @@ const { applyReporter, importMochaReporter } = require('../reporter');
|
||||
|
||||
const minimist = require('minimist');
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'vscode-file',
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true }
|
||||
},
|
||||
{
|
||||
scheme: 'vscode-remote-resource',
|
||||
privileges: { secure: true, supportFetchAPI: true, corsEnabled: true }
|
||||
}
|
||||
]);
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* _: string[];
|
||||
@@ -234,7 +246,60 @@ class IPCRunner extends events.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
app.on('ready', async () => {
|
||||
const outDir = args.build ? 'out-build' : 'out';
|
||||
const [{ createRemoteResourceRequestHandler }, { getRemoteResourceResponseHeaders }] = await Promise.all([
|
||||
import(url.pathToFileURL(path.join(__dirname, `../../../${outDir}/vs/platform/protocol/electron-main/remoteResourceProtocol.js`)).href),
|
||||
import(url.pathToFileURL(path.join(__dirname, `../../../${outDir}/vs/server/node/remoteResourceResponse.js`)).href),
|
||||
]);
|
||||
/** @type {import('http').IncomingHttpHeaders | undefined} */
|
||||
let remoteResourceRequestHeaders;
|
||||
const remoteResourceServer = http.createServer((request, response) => {
|
||||
remoteResourceRequestHeaders = request.headers;
|
||||
response.writeHead(200, {
|
||||
...getRemoteResourceResponseHeaders(request.headers.origin, () => false),
|
||||
'Content-Type': 'image/svg+xml',
|
||||
});
|
||||
response.write('<svg xmlns="http://www.w3.org/2000/svg">');
|
||||
setTimeout(() => response.end('</svg>'), 10);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
remoteResourceServer.once('error', reject);
|
||||
remoteResourceServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
const remoteResourceServerAddress = remoteResourceServer.address();
|
||||
if (!remoteResourceServerAddress || typeof remoteResourceServerAddress === 'string') {
|
||||
throw new Error('Remote resource test server did not bind to a TCP port');
|
||||
}
|
||||
protocol.handle('vscode-remote-resource', createRemoteResourceRequestHandler({ warn() { } }));
|
||||
ipcMain.handle('vscode:test-remote-resource', async () => {
|
||||
const remoteResourceTestWindow = new BrowserWindow({ show: false });
|
||||
try {
|
||||
const pagePath = url.pathToFileURL(path.join(__dirname, 'fixtures/remote-resource.html')).pathname;
|
||||
await remoteResourceTestWindow.loadURL(`vscode-file://vscode-app${pagePath}`);
|
||||
|
||||
const remoteResourceUrl = `vscode-remote-resource://127.0.0.1:${remoteResourceServerAddress.port}/vscode-remote-resource`;
|
||||
await remoteResourceTestWindow.webContents.executeJavaScript(`
|
||||
new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.onload = resolve;
|
||||
image.onerror = () => reject(new Error('Remote resource image failed to load'));
|
||||
image.src = ${JSON.stringify(remoteResourceUrl)};
|
||||
document.body.append(image);
|
||||
})
|
||||
`);
|
||||
return {
|
||||
loaded: true,
|
||||
requestHeaders: {
|
||||
origin: remoteResourceRequestHeaders?.origin,
|
||||
secFetchMode: remoteResourceRequestHeaders?.['sec-fetch-mode'],
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
remoteResourceTestWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
// needed when loading resources from the renderer, e.g xterm.js or the encoding lib
|
||||
session.defaultSession.protocol.handle('vscode-file', request => {
|
||||
@@ -440,6 +505,7 @@ app.on('ready', () => {
|
||||
if (!args.dev) {
|
||||
ipcMain.on('all done', async () => {
|
||||
await Promise.all(reporters.map(r => r.drain?.()));
|
||||
await new Promise(resolve => remoteResourceServer.close(resolve));
|
||||
app.exit(runner.didFail ? 1 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user