Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ import { ISharedTunnelsService } from '../../../platform/tunnel/common/tunnel.js
import { SharedTunnelsService } from '../../../platform/tunnel/node/tunnelService.js';
import { ipcSharedProcessTunnelChannelName, ISharedProcessTunnelService } from '../../../platform/remote/common/sharedProcessTunnelService.js';
import { SharedProcessTunnelService } from '../../../platform/tunnel/node/sharedProcessTunnelService.js';
import { ISharedProcessTunnelProxyService, ipcSharedProcessTunnelProxyChannelName } from '../../../platform/tunnel/common/sharedProcessTunnelProxyService.js';
import { SharedProcessTunnelProxyService } from '../../../platform/tunnel/node/sharedProcessTunnelProxyService.js';
import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js';
import { UriIdentityService } from '../../../platform/uriIdentity/common/uriIdentityService.js';
import { isLinux } from '../../../base/common/platform.js';
Expand Down Expand Up @@ -404,6 +406,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
remoteSocketFactoryService.register(RemoteConnectionType.WebSocket, nodeSocketFactory);
services.set(ISharedTunnelsService, new SyncDescriptor(SharedTunnelsService));
services.set(ISharedProcessTunnelService, new SyncDescriptor(SharedProcessTunnelService));
services.set(ISharedProcessTunnelProxyService, new SyncDescriptor(SharedProcessTunnelProxyService));

// Remote Tunnel
services.set(IRemoteTunnelService, new SyncDescriptor(RemoteTunnelService));
Expand Down Expand Up @@ -482,6 +485,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
const sharedProcessTunnelChannel = ProxyChannel.fromService(accessor.get(ISharedProcessTunnelService), this._store);
this.server.registerChannel(ipcSharedProcessTunnelChannelName, sharedProcessTunnelChannel);

// Tunnel Proxy
const sharedProcessTunnelProxyChannel = ProxyChannel.fromService(accessor.get(ISharedProcessTunnelProxyService), this._store);
this.server.registerChannel(ipcSharedProcessTunnelProxyChannelName, sharedProcessTunnelProxyChannel);

// Remote Tunnel
const remoteTunnelChannel = ProxyChannel.fromService(accessor.get(IRemoteTunnelService), this._store);
this.server.registerChannel('remoteTunnel', remoteTunnelChannel);
Expand Down
15 changes: 12 additions & 3 deletions src/vs/platform/browserView/common/browserView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface IBrowserViewState {
certificateError: IBrowserViewCertificateError | undefined;
storageScope: BrowserViewStorageScope;
browserZoomIndex: number;
isRemoteSession: boolean;
}

export interface IBrowserViewNavigationEvent {
Expand Down Expand Up @@ -193,6 +194,15 @@ export enum BrowserViewStorageScope {
Ephemeral = 'ephemeral'
}

export interface IBrowserSessionOptions {
/** Storage / data-isolation scope for the session. */
scope: BrowserViewStorageScope;
/** Workspace identifier — required when `scope` is `'workspace'`. */
workspaceId?: string;
/** Tunnel proxy URL for the session (only used for workspace and ephemeral scopes). */
proxyUrl?: string;
}

export const ipcBrowserViewChannelName = 'browserView';

/**
Expand Down Expand Up @@ -232,10 +242,9 @@ export interface IBrowserViewService {
/**
* Get or create a browser view instance
* @param id The browser view identifier
* @param scope The storage scope for the browser view. Ignored if the view already exists.
* @param workspaceId Workspace identifier for session isolation. Only used if scope is 'workspace'.
* @param sessionOptions Options for creating and configuring the session. Ignored if the view already exists.
*/
getOrCreateBrowserView(id: string, scope: BrowserViewStorageScope, workspaceId?: string): Promise<IBrowserViewState>;
getOrCreateBrowserView(id: string, sessionOptions: IBrowserSessionOptions): Promise<IBrowserViewState>;

/**
* Destroy a browser view instance
Expand Down
42 changes: 28 additions & 14 deletions src/vs/platform/browserView/electron-main/browserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { session } from 'electron';
import { joinPath } from '../../../base/common/resources.js';
import { URI } from '../../../base/common/uri.js';
import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js';
import { BrowserViewStorageScope } from '../common/browserView.js';
import { BrowserViewStorageScope, IBrowserSessionOptions } from '../common/browserView.js';
import { BrowserSessionTrust, IBrowserSessionTrust } from './browserSessionTrust.js';
import { FileAccess } from '../../../base/common/network.js';

Expand Down Expand Up @@ -122,25 +122,25 @@ export class BrowserSession {
/**
* Get or create a workspace-scope session for the given workspace.
*/
static getOrCreateWorkspace(workspaceId: string, workspaceStorageHome: URI): BrowserSession {
static getOrCreateWorkspace(workspaceId: string, workspaceStorageHome: URI, proxyUrl?: string): BrowserSession {
const storage = joinPath(workspaceStorageHome, workspaceId, 'browserStorage');
const electronSession = session.fromPath(storage.fsPath);
return BrowserSession._bySession.get(electronSession)
?? new BrowserSession(`workspace:${workspaceId}`, electronSession, BrowserViewStorageScope.Workspace);
?? new BrowserSession(`workspace:${workspaceId}`, electronSession, BrowserViewStorageScope.Workspace, proxyUrl);
}

/**
* Get or create an ephemeral session for the given view / target id.
*/
static getOrCreateEphemeral(viewId: string, type?: string): BrowserSession {
static getOrCreateEphemeral(viewId: string, type?: string, proxyUrl?: string): BrowserSession {
if (type === 'workspace' || type === 'ephemeral') {
throw new Error(`Cannot create session with reserved type '${type}'`);
}

const sessionId = `${type ?? 'ephemeral'}:${viewId}`;
const electronSession = session.fromPartition(`vscode-browser-${type}${viewId}`);
return BrowserSession._bySession.get(electronSession)
?? new BrowserSession(sessionId, electronSession, BrowserViewStorageScope.Ephemeral);
?? new BrowserSession(sessionId, electronSession, BrowserViewStorageScope.Ephemeral, proxyUrl);
}

/**
Expand All @@ -151,29 +151,31 @@ export class BrowserSession {
*
* @param viewId Used only for ephemeral sessions where every view
* needs its own Electron session.
* @param scope Desired storage scope.
* @param sessionOptions Determines the storage scope and proxy configuration
* for the session. The `scope` determines how the
* session `id` is derived and thus which views share
* the session.
* @param workspaceStorageHome Root folder under which per-workspace
* browser storage is created
* (`IEnvironmentMainService.workspaceStorageHome`).
* @param workspaceId Only required when `scope` is `workspace`.
*/
static getOrCreate(
viewId: string,
scope: BrowserViewStorageScope,
sessionOptions: IBrowserSessionOptions,
workspaceStorageHome: URI,
workspaceId?: string,
): BrowserSession {
switch (scope) {
switch (sessionOptions.scope) {
case BrowserViewStorageScope.Global:
return BrowserSession.getOrCreateGlobal();
case BrowserViewStorageScope.Workspace:
if (workspaceId) {
return BrowserSession.getOrCreateWorkspace(workspaceId, workspaceStorageHome);
if (sessionOptions.workspaceId) {
return BrowserSession.getOrCreateWorkspace(sessionOptions.workspaceId, workspaceStorageHome, sessionOptions.proxyUrl);
}
// fallthrough -- no workspace context -> ephemeral
case BrowserViewStorageScope.Ephemeral:
default:
return BrowserSession.getOrCreateEphemeral(viewId);
return BrowserSession.getOrCreateEphemeral(viewId, undefined, sessionOptions.proxyUrl);
}
}

Expand All @@ -183,6 +185,9 @@ export class BrowserSession {

private readonly _trust: BrowserSessionTrust;

/** Whether this session routes traffic through a remote proxy. */
readonly isRemoteSession: boolean;

private constructor(
/**
* Unique identifier for this session. Derived from what makes the
Expand All @@ -194,9 +199,12 @@ export class BrowserSession {
readonly electronSession: Electron.Session,
/** Resolved storage scope. */
readonly storageScope: BrowserViewStorageScope,
/** Proxy URL, if remote. */
proxyUrl?: string,
) {
this.isRemoteSession = !!proxyUrl;
this._trust = new BrowserSessionTrust(this);
this.configure();
this.configure(proxyUrl);
BrowserSession.knownSessions.add(electronSession);
BrowserSession._bySession.set(electronSession, this);
BrowserSession._byId.set(id, new WeakRef(this));
Expand All @@ -221,7 +229,7 @@ export class BrowserSession {
/**
* Apply the permission policy and preload scripts to the session.
*/
private configure(): void {
private configure(proxyUrl?: string): void {
this.electronSession.setPermissionRequestHandler((_webContents, permission, callback) => {
return callback(allowedPermissions.has(permission));
});
Expand All @@ -232,6 +240,12 @@ export class BrowserSession {
type: 'frame',
filePath: FileAccess.asFileUri('vs/platform/browserView/electron-browser/preload-browserView.js').fsPath
});
if (proxyUrl) {
this.electronSession.setProxy({
proxyRules: proxyUrl,
proxyBypassRules: '<-loopback>'
});
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/browserView/electron-main/browserView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ export class BrowserView extends Disposable {
lastError: this._lastError,
certificateError: this.session.trust.getCertificateError(url),
storageScope: this.session.storageScope,
browserZoomIndex: this._browserZoomIndex
browserZoomIndex: this._browserZoomIndex,
isRemoteSession: this.session.isRemoteSession
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Event } from '../../../base/common/event.js';
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
import { VSBuffer } from '../../../base/common/buffer.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { IBrowserViewBounds, IBrowserViewState, IBrowserViewService, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, BrowserViewCommandId, IElementData } from '../common/browserView.js';
import { IBrowserViewBounds, IBrowserViewState, IBrowserViewService, IBrowserViewCaptureScreenshotOptions, IBrowserViewFindInPageOptions, BrowserViewCommandId, IElementData, IBrowserSessionOptions } from '../common/browserView.js';
import { clipboard, Menu, MenuItem } from 'electron';
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js';
Expand Down Expand Up @@ -61,19 +61,18 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
super();
}

async getOrCreateBrowserView(id: string, scope: BrowserViewStorageScope, workspaceId?: string): Promise<IBrowserViewState> {
async getOrCreateBrowserView(id: string, sessionOptions: IBrowserSessionOptions): Promise<IBrowserViewState> {
if (this.browserViews.has(id)) {
// Note: scope will be ignored if the view already exists.
// Note: session options will be ignored if the view already exists.
// Browser views cannot be moved between sessions after creation.
const view = this.browserViews.get(id)!;
return view.getState();
}

const browserSession = BrowserSession.getOrCreate(
id,
scope,
sessionOptions,
this.environmentMainService.workspaceStorageHome,
workspaceId
);

const view = this.createBrowserView(id, browserSession);
Expand Down
38 changes: 38 additions & 0 deletions src/vs/platform/tunnel/common/sharedProcessTunnelProxyService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IAddress } from '../../remote/common/remoteAgentConnection.js';

export const ISharedProcessTunnelProxyService = createDecorator<ISharedProcessTunnelProxyService>('sharedProcessTunnelProxyService');

export const ipcSharedProcessTunnelProxyChannelName = 'sharedProcessTunnelProxy';

/**
* A service running in the shared process that manages a SOCKS5 proxy
* server. The proxy routes TCP connections through the remote agent
* tunnel, making the remote network transparently accessible to consumers
* that support SOCKS proxies (e.g. Electron sessions via `session.setProxy()`).
*/
export interface ISharedProcessTunnelProxyService {
readonly _serviceBrand: undefined;

/**
* Start the tunnel proxy for the given remote authority. Returns the proxy URL.
*/
start(authority: string): Promise<string>;

/**
* Set the remote address info for the proxy for the given authority.
* Should be called whenever the resolver resolves.
*/
setAddress(authority: string, address: IAddress): Promise<void>;

/**
* Release one reference to the proxy for the given authority.
* The proxy is stopped when the last reference is released.
*/
stop(authority: string): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { registerSharedProcessRemoteService } from '../../ipc/electron-browser/services.js';
import { ISharedProcessTunnelProxyService, ipcSharedProcessTunnelProxyChannelName } from '../common/sharedProcessTunnelProxyService.js';

registerSharedProcessRemoteService(ISharedProcessTunnelProxyService, ipcSharedProcessTunnelProxyChannelName);
Loading
Loading