Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/vs/code/electron-main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro
import { Schemas } from 'vs/base/common/network';
import { IFileService } from 'vs/platform/files/common/files';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { TunnelService } from 'vs/platform/remote/node/tunnelService';
import { IProductService } from 'vs/platform/product/common/productService';

class ExpectedError extends Error {
readonly isExpected = true;
Expand Down Expand Up @@ -162,6 +165,8 @@ class CodeMain {
services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
services.set(ISignService, new SyncDescriptor(SignService));
services.set(IStorageKeysSyncRegistryService, new SyncDescriptor(StorageKeysSyncRegistryService));
services.set(IProductService, { _serviceBrand: undefined, ...product });
services.set(ITunnelService, new SyncDescriptor(TunnelService));

return [new InstantiationService(services, true), instanceEnvironment, environmentService];
}
Expand Down
151 changes: 147 additions & 4 deletions src/vs/platform/remote/common/tunnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection';

export const ITunnelService = createDecorator<ITunnelService>('tunnelService');

Expand Down Expand Up @@ -35,7 +37,7 @@ export interface ITunnelService {
readonly onTunnelOpened: Event<RemoteTunnel>;
readonly onTunnelClosed: Event<{ host: string, port: number }>;

openTunnel(remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
openTunnel(resolveAuthority: IAddress | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
}
Expand All @@ -53,3 +55,144 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address:
port: +localhostMatch[2],
};
}



export abstract class AbstractTunnelService implements ITunnelService {
declare readonly _serviceBrand: undefined;

private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter();
public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event;
protected readonly _tunnels = new Map</*host*/ string, Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel> }>>();
protected _tunnelProvider: ITunnelProvider | undefined;

public constructor(
@ILogService protected readonly logService: ILogService
) { }

setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
if (!provider) {
return {
dispose: () => { }
};
}
this._tunnelProvider = provider;
return {
dispose: () => {
this._tunnelProvider = undefined;
}
};
}

public get tunnels(): Promise<readonly RemoteTunnel[]> {
const promises: Promise<RemoteTunnel>[] = [];
Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value)));
return Promise.all(promises);
}

dispose(): void {
for (const portMap of this._tunnels.values()) {
for (const { value } of portMap.values()) {
value.then(tunnel => tunnel.dispose());
}
portMap.clear();
}
this._tunnels.clear();
}

openTunnel(resolvedAuthority: IAddress | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
if (!resolvedAuthority) {
return undefined;
}

if (!remoteHost || (remoteHost === '127.0.0.1')) {
remoteHost = 'localhost';
}

const resolvedTunnel = this.retainOrCreateTunnel(resolvedAuthority, remoteHost, remotePort, localPort);
if (!resolvedTunnel) {
return resolvedTunnel;
}

return resolvedTunnel.then(tunnel => {
const newTunnel = this.makeTunnel(tunnel);
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
}
this._onTunnelOpened.fire(newTunnel);
return newTunnel;
});
}

private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel {
return {
tunnelRemotePort: tunnel.tunnelRemotePort,
tunnelRemoteHost: tunnel.tunnelRemoteHost,
tunnelLocalPort: tunnel.tunnelLocalPort,
localAddress: tunnel.localAddress,
dispose: () => {
const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost);
if (existingHost) {
const existing = existingHost.get(tunnel.tunnelRemotePort);
if (existing) {
existing.refcount--;
this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
}
}
}
};
}

private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise<RemoteTunnel> }): Promise<void> {
if (tunnel.refcount <= 0) {
const disposePromise: Promise<void> = tunnel.value.then(tunnel => {
tunnel.dispose(true);
this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
});
if (this._tunnels.has(remoteHost)) {
this._tunnels.get(remoteHost)!.delete(remotePort);
}
return disposePromise;
}
}

async closeTunnel(remoteHost: string, remotePort: number): Promise<void> {
const portMap = this._tunnels.get(remoteHost);
if (portMap && portMap.has(remotePort)) {
const value = portMap.get(remotePort)!;
value.refcount = 0;
await this.tryDisposeTunnel(remoteHost, remotePort, value);
}
}

protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise<RemoteTunnel>) {
if (!this._tunnels.has(remoteHost)) {
this._tunnels.set(remoteHost, new Map());
}
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
}

protected abstract retainOrCreateTunnel(resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
}

export class TunnelService extends AbstractTunnelService {
protected retainOrCreateTunnel(_resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel> | undefined {
const portMap = this._tunnels.get(remoteHost);
const existing = portMap ? portMap.get(remotePort) : undefined;
if (existing) {
++existing.refcount;
return existing.value;
}

if (this._tunnelProvider) {
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } });
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}
return tunnel;
}
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@
import * as net from 'net';
import { Barrier } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { findFreePortFaster } from 'vs/base/node/ports';
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { connectRemoteAgentTunnel, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { connectRemoteAgentTunnel, IAddress, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection';
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
import { ISignService } from 'vs/platform/sign/common/sign';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { findFreePortFaster } from 'vs/base/node/ports';
import { AbstractTunnelService } from 'vs/workbench/services/remote/common/tunnelService';

async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
Expand Down Expand Up @@ -128,16 +124,14 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {

export class TunnelService extends AbstractTunnelService {
public constructor(
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@ILogService logService: ILogService,
@IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@ISignService private readonly signService: ISignService,
@IProductService private readonly productService: IProductService
) {
super(environmentService, logService);
super(logService);
}

protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
protected retainOrCreateTunnel(resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
const portMap = this._tunnels.get(remoteHost);
const existing = portMap ? portMap.get(remotePort) : undefined;
if (existing) {
Expand All @@ -157,8 +151,7 @@ export class TunnelService extends AbstractTunnelService {
socketFactory: nodeSocketFactory,
addressProvider: {
getAddress: async () => {
const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority);
return { host: authority.host, port: authority.port };
return resolveRemoteAuthority;
}
},
signService: this.signService,
Expand All @@ -171,5 +164,3 @@ export class TunnelService extends AbstractTunnelService {
}
}
}

registerSingleton(ITunnelService, TunnelService, true);
4 changes: 4 additions & 0 deletions src/vs/platform/webview/common/resourceLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ 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';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a temporary partition on purpose, because is only to intercept requests in a isolated way and not for persisting data to disk ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we use partition to make sure that our onBeforeRequest handler only intercepts requests from webviews instead of being fired for all requests

I've also added a filter so that we only intercept localhost requests from webviews

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to note, if the partition is not persistent that is, if it doesn't start with persist:webview, then the storage for webviews will be temporary, cookies, web storage etc, network auth cache etc or any user data related to the browsing context will not be persisted to disk. Is that the intention here ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, right now webviews are supposed to be entirely ephemeral

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, thanks for confirming!



export namespace WebviewResourceResponse {
export enum Type { Success, Failed, AccessDenied }

Expand Down
4 changes: 3 additions & 1 deletion src/vs/platform/webview/common/webviewManagerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import { UriComponents } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping';

export const IWebviewManagerService = createDecorator<IWebviewManagerService>('webviewManagerService');

export interface IWebviewManagerService {
_serviceBrand: unknown;

registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise<void>;
registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void>;
unregisterWebview(id: string): Promise<void>;
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;

Expand All @@ -23,4 +24,5 @@ export interface RegisterWebviewMetadata {
readonly extensionLocation: UriComponents | undefined;
readonly localResourceRoots: readonly UriComponents[];
readonly remoteConnectionData: IRemoteConnectionData | null;
readonly portMappings: readonly IWebviewPortMapping[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,42 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { ITunnelService, RemoteTunnel, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/remote/common/tunnel';
import { extractLocalHostUriMetaDataForPortMapping, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';

export class WebviewPortMappingManager extends Disposable {
export interface IWebviewPortMapping {
webviewPort: number;
extensionHostPort: number;
}

/**
* Manages port mappings for a single webview.
*/
export class WebviewPortMappingManager implements IDisposable {

private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();

constructor(
private readonly getExtensionLocation: () => URI | undefined,
private readonly mappings: () => ReadonlyArray<modes.IWebviewPortMapping>,
private readonly _getExtensionLocation: () => URI | undefined,
private readonly _getMappings: () => readonly IWebviewPortMapping[],
private readonly tunnelService: ITunnelService
) {
super();
}
) { }

public async getRedirect(url: string): Promise<string | undefined> {
public async getRedirect(resolveAuthority: IAddress, url: string): Promise<string | undefined> {
const uri = URI.parse(url);
const requestLocalHostInfo = extractLocalHostUriMetaDataForPortMapping(uri);
if (!requestLocalHostInfo) {
return undefined;
}

for (const mapping of this.mappings()) {
for (const mapping of this._getMappings()) {
if (mapping.webviewPort === requestLocalHostInfo.port) {
const extensionLocation = this.getExtensionLocation();
const extensionLocation = this._getExtensionLocation();
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort);
const tunnel = await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort);
if (tunnel) {
if (tunnel.tunnelLocalPort === mapping.webviewPort) {
return undefined;
Expand All @@ -55,20 +61,18 @@ export class WebviewPortMappingManager extends Disposable {
}

dispose() {
super.dispose();

for (const tunnel of this._tunnels.values()) {
tunnel.then(tunnel => tunnel.dispose());
}
this._tunnels.clear();
}

private getOrCreateTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
private getOrCreateTunnel(remoteAuthority: IAddress, remotePort: number): Promise<RemoteTunnel> | undefined {
const existing = this._tunnels.get(remotePort);
if (existing) {
return existing;
}
const tunnel = this.tunnelService.openTunnel(undefined, remotePort);
const tunnel = this.tunnelService.openTunnel(remoteAuthority, undefined, remotePort);
if (tunnel) {
this._tunnels.set(remotePort, tunnel);
}
Expand Down
Loading