Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"documentDiff",
"documentSyntaxHighlighting",
"textEditorDiffInformation",
"customEditorPriority"
"customEditorPriority",
"webviewNoServiceWorker"
],
"engines": {
"vscode": "^1.70.0"
Expand Down
1 change: 1 addition & 0 deletions src/vs/code/electron-browser/workbench/workbench-dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
notebookChatEditController
richScreenReaderContent
chatDebugTokenizer
singleIframeWebview
;
"/>
</head>
Expand Down
1 change: 1 addition & 0 deletions src/vs/code/electron-browser/workbench/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
notebookChatEditController
richScreenReaderContent
chatDebugTokenizer
singleIframeWebview
;
"/>

Expand Down
24 changes: 24 additions & 0 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { NativeURLService } from '../../platform/url/common/urlService.js';
import { ElectronURLListener } from '../../platform/url/electron-main/electronUrlListener.js';
import { IWebviewManagerService } from '../../platform/webview/common/webviewManagerService.js';
import { WebviewMainService } from '../../platform/webview/electron-main/webviewMainService.js';
import { WebviewProtocolProvider } from '../../platform/webview/electron-main/webviewProtocolProvider.js';
import { isFolderToOpen, isWorkspaceToOpen, IWindowOpenable } from '../../platform/window/common/window.js';
import { getAllWindowsExcludingOffscreen, IWindowsMainService, OpenContext } from '../../platform/windows/electron-main/windows.js';
import { ICodeWindow } from '../../platform/window/electron-main/window.js';
Expand Down Expand Up @@ -387,6 +388,22 @@ export class CodeApplication extends Disposable {
};

const isAllowedWebviewRequest = (uri: URI, details: Electron.OnBeforeRequestListenerDetails): boolean => {
const directDocument = WebviewProtocolProvider.getWebviewDocument(uri);
if (directDocument) {
const frame = details.frame;
const owner = this.windowsMainService?.getWindowById(directDocument.windowId)?.win;
if (!frame || !owner) {
return false;
}
if (frame.frameTreeNodeId !== directDocument.frameTreeNodeId) {
return false;
}
const route = `${Schemas.vscodeWebview}://${directDocument.extensionId.toLowerCase()}/${encodeURIComponent(directDocument.webviewId)}/`;
const isInitialNavigation = frame.url === ''
|| frame.url === 'about:blank'
|| frame.url.startsWith(`${Schemas.vscodeFileResource}://`);
return isInitialNavigation || frame.url.startsWith(route);
}
if (uri.path !== '/index.html') {
return true; // Only restrict top level page of webviews: index.html
}
Expand All @@ -410,6 +427,13 @@ export class CodeApplication extends Disposable {

session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
const uri = URI.parse(details.url);
if ((uri.scheme === Schemas.http || uri.scheme === Schemas.https) && details.frame) {
const portMapping = WebviewProtocolProvider.getWebviewPortMapping(details.frame.url, details.url);
if (portMapping) {
void portMapping.then(redirectURL => callback(redirectURL ? { redirectURL } : { cancel: false }));
return;
}
}
if (uri.scheme === Schemas.vscodeWebview) {
if (!isAllowedWebviewRequest(uri, details)) {
this.logService.error('Blocked vscode-webview request', details.url);
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/extensions/common/extensionsApiProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ const _allApiProposals = {
valueSelectionInQuickPick: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.valueSelectionInQuickPick.d.ts',
},
webviewNoServiceWorker: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.webviewNoServiceWorker.d.ts',
},
workspaceTrust: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.workspaceTrust.d.ts',
}
Expand Down
33 changes: 33 additions & 0 deletions src/vs/platform/webview/common/resourceLoading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { isUNC } from '../../../base/common/extpath.js';
import { Schemas } from '../../../base/common/network.js';
import { URI } from '../../../base/common/uri.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';

export function isWebviewResourceAllowed(resource: URI, roots: readonly URI[], uriIdentityService: IUriIdentityService): boolean {
const resourceWithoutQuery = resource.with({ query: '' });
for (const root of roots) {
if (uriIdentityService.extUri.isEqual(root, resourceWithoutQuery, true)) {
continue;
}

// Compare UNC paths case-insensitively.
if (root.scheme === Schemas.file && isUNC(root.fsPath)) {
if (resourceWithoutQuery.scheme === Schemas.file && isUNC(resourceWithoutQuery.fsPath)
&& uriIdentityService.extUri.isEqualOrParent(
resourceWithoutQuery.with({ path: resourceWithoutQuery.path.toLowerCase(), authority: resourceWithoutQuery.authority.toLowerCase() }),
root.with({ path: root.path.toLowerCase(), authority: root.authority.toLowerCase() }),
true,
)) {
return true;
}
} else if (uriIdentityService.extUri.isEqualOrParent(resourceWithoutQuery, root, true)) {
return true;
}
}
return false;
}
49 changes: 49 additions & 0 deletions src/vs/platform/webview/common/webviewManagerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import { Event } from '../../../base/common/event.js';
import { VSBuffer } from '../../../base/common/buffer.js';
import { UriComponents } from '../../../base/common/uri.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';

export const IWebviewManagerService = createDecorator<IWebviewManagerService>('webviewManagerService');
Expand Down Expand Up @@ -33,10 +35,57 @@ export interface IWebviewManagerService {
_serviceBrand: unknown;

readonly onFoundInFrame: Event<FoundInFrameResult>;
readonly onDidRequestWebviewResource: Event<WebviewResourceRequest>;
readonly onDidCancelWebviewResource: Event<number>;
readonly onDidRequestWebviewPortMapping: Event<WebviewPortMappingRequest>;

registerWebviewDocument(document: WebviewDocumentRegistration): Promise<void>;
unregisterWebviewDocument(extensionId: string, webviewId: string): Promise<void>;
startWebviewResourceResponse(response: WebviewResourceResponse): Promise<void>;
streamWebviewResourceResponse(requestId: number, data: VSBuffer): Promise<void>;
endWebviewResourceResponse(requestId: number, error?: boolean): Promise<void>;
resolveWebviewPortMapping(requestId: number, redirect: string | undefined): Promise<void>;

setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise<void>;

findInFrame(windowId: WebviewWindowId, frameName: string, text: string, options: FindInFrameOptions): Promise<void>;

stopFindInFrame(windowId: WebviewWindowId, frameName: string, options: { keepSelection?: boolean }): Promise<void>;
}

export interface WebviewDocumentRegistration {
readonly extensionId: string;
readonly webviewId: string;
readonly windowId: number;
readonly frameName: string;
readonly html: string;
readonly csp: string;
readonly roots: readonly UriComponents[];
}

export interface WebviewResourceRequest {
readonly requestId: number;
readonly extensionId: string;
readonly webviewId: string;
readonly method: 'GET' | 'HEAD';
readonly uri: UriComponents;
readonly ifNoneMatch: string | undefined;
readonly range: { readonly start: number; readonly end?: number } | undefined;
}

export interface WebviewResourceResponse {
readonly requestId: number;
readonly status: number;
readonly mime: string | undefined;
readonly etag: string | undefined;
readonly mtime: number | undefined;
readonly size: number | undefined;
readonly range: string | undefined;
}

export interface WebviewPortMappingRequest {
readonly requestId: number;
readonly extensionId: string;
readonly webviewId: string;
readonly origin: string;
}
55 changes: 53 additions & 2 deletions src/vs/platform/webview/electron-main/webviewMainService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,75 @@
import { WebContents, webContents, WebFrameMain } from 'electron';
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { FindInFrameOptions, FoundInFrameResult, IWebviewManagerService, WebviewWebContentsId, WebviewWindowId } from '../common/webviewManagerService.js';
import { FindInFrameOptions, FoundInFrameResult, IWebviewManagerService, WebviewDocumentRegistration, WebviewPortMappingRequest, WebviewResourceRequest, WebviewResourceResponse, WebviewWebContentsId, WebviewWindowId } from '../common/webviewManagerService.js';
import { WebviewProtocolProvider } from './webviewProtocolProvider.js';
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
import { IFileService } from '../../files/common/files.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';

export class WebviewMainService extends Disposable implements IWebviewManagerService {

declare readonly _serviceBrand: undefined;

private readonly _onFoundInFrame = this._register(new Emitter<FoundInFrameResult>());
public readonly onFoundInFrame = this._onFoundInFrame.event;
private readonly _onDidRequestWebviewResource = this._register(new Emitter<WebviewResourceRequest>());
public readonly onDidRequestWebviewResource = this._onDidRequestWebviewResource.event;
private readonly _onDidCancelWebviewResource = this._register(new Emitter<number>());
public readonly onDidCancelWebviewResource = this._onDidCancelWebviewResource.event;
private readonly _onDidRequestWebviewPortMapping = this._register(new Emitter<WebviewPortMappingRequest>());
public readonly onDidRequestWebviewPortMapping = this._onDidRequestWebviewPortMapping.event;
private readonly protocolProvider: WebviewProtocolProvider;

constructor(
@IFileService fileService: IFileService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
) {
super();
this._register(new WebviewProtocolProvider(fileService));
this.protocolProvider = this._register(new WebviewProtocolProvider(
request => this._onDidRequestWebviewResource.fire(request),
requestId => this._onDidCancelWebviewResource.fire(requestId),
request => this._onDidRequestWebviewPortMapping.fire(request),
uriIdentityService,
fileService,
));
this._register(this.windowsMainService.onDidDestroyWindow(window => this.protocolProvider.unregisterWebviewWindow(window.id)));
}

public async registerWebviewDocument(document: WebviewDocumentRegistration): Promise<void> {
const window = this.windowsMainService.getWindowById(document.windowId);
const mainFrame = window?.win?.webContents.mainFrame;
const frame = mainFrame?.framesInSubtree.find(frame => {
return frame.parent === mainFrame && frame.name === document.frameName;
});
if (!frame) {
throw new Error(`Unknown direct webview frame: ${document.frameName}`);
}
this.protocolProvider.registerWebviewDocument({
...document,
frameTreeNodeId: frame.frameTreeNodeId,
});
}

public async unregisterWebviewDocument(extensionId: string, webviewId: string): Promise<void> {
this.protocolProvider.unregisterWebviewDocument(extensionId, webviewId);
}

public async startWebviewResourceResponse(response: WebviewResourceResponse): Promise<void> {
this.protocolProvider.startResourceResponse(response);
}

public async streamWebviewResourceResponse(requestId: number, data: import('../../../base/common/buffer.js').VSBuffer): Promise<void> {
this.protocolProvider.streamResourceResponse(requestId, data);
}

public async endWebviewResourceResponse(requestId: number, error?: boolean): Promise<void> {
this.protocolProvider.endResourceResponse(requestId, error);
}

public async resolveWebviewPortMapping(requestId: number, redirect: string | undefined): Promise<void> {
this.protocolProvider.resolvePortMapping(requestId, redirect);
}

public async setIgnoreMenuShortcuts(id: WebviewWebContentsId | WebviewWindowId, enabled: boolean): Promise<void> {
Expand Down
Loading