mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 09:35:40 -05:00
Merge from vscode 2cd495805cf99b31b6926f08ff4348124b2cf73d
This commit is contained in:
committed by
AzureDataStudio
parent
a8a7559229
commit
1388493cc1
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isUNC } from 'vs/base/common/extpath';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@@ -11,14 +11,12 @@ import { sep } from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
|
||||
|
||||
|
||||
export const webviewPartitionId = 'webview';
|
||||
|
||||
|
||||
export namespace WebviewResourceResponse {
|
||||
export enum Type { Success, Failed, AccessDenied }
|
||||
|
||||
@@ -31,73 +29,41 @@ export namespace WebviewResourceResponse {
|
||||
) { }
|
||||
}
|
||||
|
||||
export class BufferSuccess {
|
||||
readonly type = Type.Success;
|
||||
|
||||
constructor(
|
||||
public readonly buffer: VSBuffer,
|
||||
public readonly mimeType: string
|
||||
) { }
|
||||
}
|
||||
|
||||
export const Failed = { type: Type.Failed } as const;
|
||||
export const AccessDenied = { type: Type.AccessDenied } as const;
|
||||
|
||||
export type BufferResponse = BufferSuccess | typeof Failed | typeof AccessDenied;
|
||||
export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied;
|
||||
}
|
||||
|
||||
export async function loadLocalResource(
|
||||
requestUri: URI,
|
||||
fileService: IFileService,
|
||||
extensionLocation: URI | undefined,
|
||||
roots: ReadonlyArray<URI>
|
||||
): Promise<WebviewResourceResponse.BufferResponse> {
|
||||
const resourceToLoad = getResourceToLoad(requestUri, extensionLocation, roots);
|
||||
if (!resourceToLoad) {
|
||||
return WebviewResourceResponse.AccessDenied;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fileService.readFile(resourceToLoad);
|
||||
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
|
||||
return new WebviewResourceResponse.BufferSuccess(data.value, mime);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return WebviewResourceResponse.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadLocalResourceStream(
|
||||
requestUri: URI,
|
||||
options: {
|
||||
extensionLocation: URI | undefined;
|
||||
roots: ReadonlyArray<URI>;
|
||||
remoteConnectionData?: IRemoteConnectionData | null;
|
||||
rewriteUri?: (uri: URI) => URI,
|
||||
},
|
||||
fileService: IFileService,
|
||||
requestService: IRequestService,
|
||||
): Promise<WebviewResourceResponse.StreamResponse> {
|
||||
const resourceToLoad = getResourceToLoad(requestUri, options.extensionLocation, options.roots);
|
||||
let resourceToLoad = getResourceToLoad(requestUri, options.roots);
|
||||
if (!resourceToLoad) {
|
||||
return WebviewResourceResponse.AccessDenied;
|
||||
}
|
||||
|
||||
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
|
||||
|
||||
if (options.remoteConnectionData) {
|
||||
// Remote uris must go to the resolved server.
|
||||
if (resourceToLoad.scheme === Schemas.vscodeRemote || (options.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
|
||||
const uri = URI.parse(`http://${options.remoteConnectionData.host}:${options.remoteConnectionData.port}`).with({
|
||||
path: '/vscode-remote-resource',
|
||||
query: `tkn=${options.remoteConnectionData.connectionToken}&path=${encodeURIComponent(resourceToLoad.path)}`,
|
||||
});
|
||||
// Perform extra normalization if needed
|
||||
if (options.rewriteUri) {
|
||||
resourceToLoad = options.rewriteUri(resourceToLoad);
|
||||
}
|
||||
|
||||
const response = await requestService.request({ url: uri.toString(true) }, CancellationToken.None);
|
||||
if (response.res.statusCode === 200) {
|
||||
return new WebviewResourceResponse.StreamSuccess(response.stream, mime);
|
||||
}
|
||||
return WebviewResourceResponse.Failed;
|
||||
if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) {
|
||||
const response = await requestService.request({ url: resourceToLoad.toString(true) }, CancellationToken.None);
|
||||
if (response.res.statusCode === 200) {
|
||||
return new WebviewResourceResponse.StreamSuccess(response.stream, mime);
|
||||
}
|
||||
return WebviewResourceResponse.Failed;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -111,7 +77,6 @@ export async function loadLocalResourceStream(
|
||||
|
||||
function getResourceToLoad(
|
||||
requestUri: URI,
|
||||
extensionLocation: URI | undefined,
|
||||
roots: ReadonlyArray<URI>
|
||||
): URI | undefined {
|
||||
const normalizedPath = normalizeRequestPath(requestUri);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const IWebviewManagerService = createDecorator<IWebviewManagerService>('w
|
||||
export interface IWebviewManagerService {
|
||||
_serviceBrand: unknown;
|
||||
|
||||
registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void>;
|
||||
registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void>;
|
||||
unregisterWebview(id: string): Promise<void>;
|
||||
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export class WebviewPortMappingManager implements IDisposable {
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const tunnel = this.tunnelService.openTunnel(remoteAuthority, undefined, remotePort);
|
||||
const tunnel = this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort);
|
||||
if (tunnel) {
|
||||
this._tunnels.set(remotePort, tunnel);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
|
||||
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
|
||||
}
|
||||
|
||||
public async registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void> {
|
||||
public async registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void> {
|
||||
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
|
||||
|
||||
this.protocolProvider.registerWebview(id, {
|
||||
|
||||
@@ -20,7 +20,7 @@ interface PortMappingData {
|
||||
export class WebviewPortMappingProvider extends Disposable {
|
||||
|
||||
private readonly _webviewData = new Map<string, {
|
||||
readonly webContentsId: number;
|
||||
readonly webContentsId: number | undefined;
|
||||
readonly manager: WebviewPortMappingManager;
|
||||
metadata: PortMappingData;
|
||||
}>();
|
||||
@@ -56,14 +56,16 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public async registerWebview(id: string, webContentsId: number, metadata: PortMappingData): Promise<void> {
|
||||
public async registerWebview(id: string, webContentsId: number | undefined, metadata: PortMappingData): Promise<void> {
|
||||
const manager = new WebviewPortMappingManager(
|
||||
() => this._webviewData.get(id)?.metadata.extensionLocation,
|
||||
() => this._webviewData.get(id)?.metadata.mappings || [],
|
||||
this._tunnelService);
|
||||
|
||||
this._webviewData.set(id, { webContentsId, metadata, manager });
|
||||
this._webContentsIdsToWebviewIds.set(webContentsId, id);
|
||||
if (typeof webContentsId === 'number') {
|
||||
this._webContentsIdsToWebviewIds.set(webContentsId, id);
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterWebview(id: string): void {
|
||||
@@ -71,7 +73,9 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||
if (existing) {
|
||||
existing.manager.dispose();
|
||||
this._webviewData.delete(id);
|
||||
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
|
||||
if (typeof existing.webContentsId === 'number') {
|
||||
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { session } from 'electron';
|
||||
import { session, protocol } from 'electron';
|
||||
import { Readable } from 'stream';
|
||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -12,7 +12,8 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { loadLocalResourceStream, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
|
||||
import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
|
||||
interface WebviewMetadata {
|
||||
readonly extensionLocation: URI | undefined;
|
||||
@@ -22,6 +23,13 @@ interface WebviewMetadata {
|
||||
|
||||
export class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
private static validWebviewFilePaths = new Map([
|
||||
['/index.html', 'index.html'],
|
||||
['/electron-browser/index.html', 'index.html'],
|
||||
['/main.js', 'main.js'],
|
||||
['/host.js', 'host.js'],
|
||||
]);
|
||||
|
||||
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
|
||||
|
||||
constructor(
|
||||
@@ -32,42 +40,22 @@ export class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
const sess = session.fromPartition(webviewPartitionId);
|
||||
|
||||
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
// Register the protocol loading webview html
|
||||
const webviewHandler = this.handleWebviewRequest.bind(this);
|
||||
protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
|
||||
sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
|
||||
|
||||
const id = uri.authority;
|
||||
const metadata = this.webviewMetadata.get(id);
|
||||
if (metadata) {
|
||||
const result = await loadLocalResourceStream(uri, {
|
||||
extensionLocation: metadata.extensionLocation,
|
||||
roots: metadata.localResourceRoots,
|
||||
remoteConnectionData: metadata.remoteConnectionData,
|
||||
}, this.fileService, this.requestService);
|
||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||
return callback({
|
||||
statusCode: 200,
|
||||
data: this.streamToNodeReadable(result.stream),
|
||||
headers: {
|
||||
'Content-Type': result.mimeType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
}
|
||||
// Register the protocol loading webview resources both inside the webview and at the top level
|
||||
const webviewResourceHandler = this.handleWebviewResourceRequest.bind(this);
|
||||
protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
|
||||
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||
console.error('Webview: Cannot load resource outside of protocol root');
|
||||
return callback({ data: null, statusCode: 401 });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return callback({ data: null, statusCode: 404 });
|
||||
});
|
||||
|
||||
this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
|
||||
this._register(toDisposable(() => {
|
||||
protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
|
||||
sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
|
||||
protocol.unregisterProtocol(Schemas.vscodeWebview);
|
||||
sess.protocol.unregisterProtocol(Schemas.vscodeWebview);
|
||||
}));
|
||||
}
|
||||
|
||||
private streamToNodeReadable(stream: VSBufferReadableStream): Readable {
|
||||
@@ -131,4 +119,81 @@ export class WebviewProtocolProvider extends Disposable {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWebviewRequest(request: Electron.Request, callback: any) {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
|
||||
if (typeof entry === 'string') {
|
||||
let url: string;
|
||||
if (uri.path.startsWith('/electron-browser')) {
|
||||
url = require.toUrl(`vs/workbench/contrib/webview/electron-browser/pre/${entry}`);
|
||||
} else {
|
||||
url = require.toUrl(`vs/workbench/contrib/webview/browser/pre/${entry}`);
|
||||
}
|
||||
return callback(decodeURIComponent(url.replace('file://', '')));
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ });
|
||||
}
|
||||
|
||||
private async handleWebviewResourceRequest(
|
||||
request: Electron.Request,
|
||||
callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void
|
||||
) {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
|
||||
const id = uri.authority;
|
||||
const metadata = this.webviewMetadata.get(id);
|
||||
if (metadata) {
|
||||
|
||||
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
|
||||
let rewriteUri: undefined | ((uri: URI) => URI);
|
||||
if (metadata.remoteConnectionData) {
|
||||
rewriteUri = (uri) => {
|
||||
if (metadata.remoteConnectionData) {
|
||||
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
|
||||
const scheme = metadata.remoteConnectionData.host === 'localhost' || metadata.remoteConnectionData.host === '127.0.0.1' ? 'http' : 'https';
|
||||
return URI.parse(`${scheme}://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
|
||||
path: '/vscode-remote-resource',
|
||||
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
};
|
||||
}
|
||||
|
||||
const result = await loadLocalResource(uri, {
|
||||
extensionLocation: metadata.extensionLocation,
|
||||
roots: metadata.localResourceRoots,
|
||||
remoteConnectionData: metadata.remoteConnectionData,
|
||||
rewriteUri,
|
||||
}, this.fileService, this.requestService);
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||
return callback({
|
||||
statusCode: 200,
|
||||
data: this.streamToNodeReadable(result.stream),
|
||||
headers: {
|
||||
'Content-Type': result.mimeType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||
console.error('Webview: Cannot load resource outside of protocol root');
|
||||
return callback({ data: null, statusCode: 401 });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return callback({ data: null, statusCode: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user