未验证 提交 a7cf03de 编写于 作者: A Alex Ross 提交者: GitHub

Add elevation message to ports UI (#113990)

This change allows remote extension to handle elevation if they want.
Fixes microsoft/vscode-remote-release#3922
上级 efd298cc
......@@ -5,6 +5,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isWindows } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
......@@ -30,6 +31,10 @@ export interface TunnelCreationOptions {
elevationRequired?: boolean;
}
export interface TunnelProviderFeatures {
elevation: boolean;
}
export interface ITunnelProvider {
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined;
}
......@@ -40,10 +45,11 @@ export interface ITunnelService {
readonly tunnels: Promise<readonly RemoteTunnel[]>;
readonly onTunnelOpened: Event<RemoteTunnel>;
readonly onTunnelClosed: Event<{ host: string, port: number }>;
readonly canElevate: boolean;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel | undefined> | undefined;
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean): Promise<RemoteTunnel | undefined> | undefined;
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable;
}
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined {
......@@ -74,6 +80,10 @@ function getOtherLocalhost(host: string): string | undefined {
return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined);
}
export function isPortPrivileged(port: number): boolean {
return !isWindows && (port < 1024);
}
export abstract class AbstractTunnelService implements ITunnelService {
declare readonly _serviceBrand: undefined;
......@@ -83,18 +93,22 @@ export abstract class AbstractTunnelService implements ITunnelService {
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 | undefined> }>>();
protected _tunnelProvider: ITunnelProvider | undefined;
protected _canElevate: boolean = false;
public constructor(
@ILogService protected readonly logService: ILogService
) { }
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable {
this._tunnelProvider = provider;
if (!provider) {
// clear features
this._canElevate = false;
return {
dispose: () => { }
};
}
this._tunnelProvider = provider;
this._canElevate = features.elevation;
return {
dispose: () => {
this._tunnelProvider = undefined;
......@@ -102,6 +116,10 @@ export abstract class AbstractTunnelService implements ITunnelService {
};
}
public get canElevate(): boolean {
return this._canElevate;
}
public get tunnels(): Promise<readonly RemoteTunnel[]> {
return new Promise(async (resolve) => {
const tunnels: RemoteTunnel[] = [];
......@@ -129,7 +147,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
this._tunnels.clear();
}
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel | undefined> | undefined {
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false): Promise<RemoteTunnel | undefined> | undefined {
if (!addressProvider) {
return undefined;
}
......@@ -138,7 +156,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
remoteHost = 'localhost';
}
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort);
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded);
if (!resolvedTunnel) {
return resolvedTunnel;
}
......@@ -238,15 +256,11 @@ export abstract class AbstractTunnelService implements ITunnelService {
return portMap ? portMap.get(remotePort) : undefined;
}
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel | undefined> | undefined;
protected isPortPrivileged(port: number): boolean {
return port < 1024;
}
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean): Promise<RemoteTunnel | undefined> | undefined;
}
export class TunnelService extends AbstractTunnelService {
protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel | undefined> | undefined {
protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean): Promise<RemoteTunnel | undefined> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
......@@ -256,7 +270,7 @@ export class TunnelService extends AbstractTunnelService {
if (this._tunnelProvider) {
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
......
......@@ -11,7 +11,7 @@ import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { AbstractTunnelService, isPortPrivileged, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
import { ISignService } from 'vs/platform/sign/common/sign';
......@@ -139,7 +139,7 @@ export class BaseTunnelService extends AbstractTunnelService {
super(logService);
}
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel | undefined> | undefined {
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean): Promise<RemoteTunnel | undefined> | undefined {
const existing = this.getTunnelFromMap(remoteHost, remotePort);
if (existing) {
++existing.refcount;
......@@ -148,7 +148,7 @@ export class BaseTunnelService extends AbstractTunnelService {
if (this._tunnelProvider) {
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
if (tunnel) {
......
......@@ -234,10 +234,18 @@ declare module 'vscode' {
*/
tunnelFactory?: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => Thenable<Tunnel> | undefined;
/**
/**p
* Provides filtering for candidate ports.
*/
showCandidatePort?: (host: string, port: number, detail: string) => Thenable<boolean>;
/**
* Lets the resolver declare which tunnel factory features it supports.
* UNDER DISCUSSION! MAY CHANGE SOON.
*/
tunnelFeatures?: {
elevation: boolean;
};
}
export namespace workspace {
......
......@@ -7,7 +7,7 @@ import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostCont
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { CandidatePort, IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver';
......@@ -52,7 +52,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
this.remoteExplorerService.onFoundNewCandidates(candidates);
}
async $setTunnelProvider(): Promise<void> {
async $setTunnelProvider(features: TunnelProviderFeatures): Promise<void> {
const tunnelProvider: ITunnelProvider = {
forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => {
const forward = this._proxy.$forwardPort(tunnelOptions, tunnelCreationOptions);
......@@ -75,7 +75,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
return undefined;
}
};
this.tunnelService.setTunnelProvider(tunnelProvider);
this.tunnelService.setTunnelProvider(tunnelProvider, features);
}
dispose(): void {
......
......@@ -48,7 +48,7 @@ import * as search from 'vs/workbench/services/search/common/search';
import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor';
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline';
import { revive } from 'vs/base/common/marshalling';
import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon';
......@@ -972,7 +972,7 @@ export interface MainThreadTunnelServiceShape extends IDisposable {
$openTunnel(tunnelOptions: TunnelOptions, source: string | undefined): Promise<TunnelDto | undefined>;
$closeTunnel(remote: { host: string, port: number }): Promise<void>;
$getTunnels(): Promise<TunnelDescription[]>;
$setTunnelProvider(): Promise<void>;
$setTunnelProvider(features: TunnelProviderFeatures): Promise<void>;
$onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise<void>;
}
......
......@@ -195,7 +195,9 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
}
if (provider.tunnelFactory) {
this._forwardPortProvider = provider.tunnelFactory;
await this._proxy.$setTunnelProvider();
await this._proxy.$setTunnelProvider(provider.tunnelFeatures ?? {
elevation: false
});
}
} else {
this._forwardPortProvider = undefined;
......
......@@ -21,7 +21,7 @@ import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal
import { IDebugService } from 'vs/workbench/contrib/debug/common/debug';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { OperatingSystem } from 'vs/base/common/platform';
import { RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { isPortPrivileged, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
......@@ -202,7 +202,8 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon
@IContextKeyService readonly contextKeyService: IContextKeyService,
@IConfigurationService readonly configurationService: IConfigurationService,
@IDebugService readonly debugService: IDebugService,
@IRemoteAgentService readonly remoteAgentService: IRemoteAgentService
@IRemoteAgentService readonly remoteAgentService: IRemoteAgentService,
@ITunnelService readonly tunnelService: ITunnelService
) {
super();
if (!this.environmentService.remoteAuthority) {
......@@ -212,9 +213,9 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon
remoteAgentService.getEnvironment().then(environment => {
if (environment?.os === OperatingSystem.Windows) {
this._register(new WindowsAutomaticPortForwarding(terminalService, notificationService, openerService,
remoteExplorerService, configurationService, debugService));
remoteExplorerService, configurationService, debugService, tunnelService));
} else if (environment?.os === OperatingSystem.Linux) {
this._register(new LinuxAutomaticPortForwarding(configurationService, remoteExplorerService, notificationService, openerService));
this._register(new LinuxAutomaticPortForwarding(configurationService, remoteExplorerService, notificationService, openerService, tunnelService));
}
});
}
......@@ -228,7 +229,8 @@ class ForwardedPortNotifier extends Disposable {
constructor(private readonly notificationService: INotificationService,
private readonly remoteExplorerService: IRemoteExplorerService,
private readonly openerService: IOpenerService) {
private readonly openerService: IOpenerService,
private readonly tunnelService: ITunnelService) {
super();
this.lastNotifyTime = new Date();
this.lastNotifyTime.setFullYear(this.lastNotifyTime.getFullYear() - 1);
......@@ -278,18 +280,31 @@ class ForwardedPortNotifier extends Disposable {
});
}
private basicMessage(tunnel: RemoteTunnel) {
return nls.localize('remote.tunnelsView.automaticForward', "Your service running on port {0} is available. ",
tunnel.tunnelRemotePort);
}
private linkMessage() {
return nls.localize('remote.tunnelsView.notificationLink', "[See all forwarded ports](command:{0}.focus)", TunnelPanel.ID);
}
private showNotification(tunnel: RemoteTunnel) {
if (this.lastNotification) {
this.lastNotification.close();
}
const address = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
const message = nls.localize('remote.tunnelsView.automaticForward', "Your service running on port {0} is available. [See all forwarded ports](command:{1}.focus)",
tunnel.tunnelRemotePort, TunnelPanel.ID);
const browserChoice: IPromptChoice = {
label: OpenPortInBrowserAction.LABEL,
run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address)
};
this.lastNotification = this.notificationService.prompt(Severity.Info, message, [browserChoice], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } });
let message = this.basicMessage(tunnel);
const choices = [this.openChoice(tunnel)];
if (tunnel.tunnelLocalPort !== undefined && this.tunnelService.canElevate && isPortPrivileged(tunnel.tunnelRemotePort)) {
// Privileged ports are not on Windows, so it's safe to use "superuser"
message += nls.localize('remote.tunnelsView.elevationMessage', "You'll need to run as superuser to use port {0} locally. ", tunnel.tunnelRemotePort);
choices.unshift(this.elevateChoice(tunnel));
}
message += this.linkMessage();
this.lastNotification = this.notificationService.prompt(Severity.Info, message, choices, { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } });
this.lastShownPort = tunnel.tunnelRemotePort;
this.lastNotifyTime = new Date();
this.lastNotification.onDidClose(() => {
......@@ -297,6 +312,37 @@ class ForwardedPortNotifier extends Disposable {
this.lastShownPort = undefined;
});
}
private openChoice(tunnel: RemoteTunnel): IPromptChoice {
const address = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
return {
label: OpenPortInBrowserAction.LABEL,
run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address)
};
}
private elevateChoice(tunnel: RemoteTunnel): IPromptChoice {
return {
// Privileged ports are not on Windows, so it's ok to stick to just "sudo".
label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort),
run: async () => {
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
const newTunnel = await this.remoteExplorerService.forward({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, tunnel.tunnelRemotePort, undefined, undefined, true);
if (!newTunnel) {
return;
}
if (this.lastNotification) {
this.lastNotification.close();
}
this.lastShownPort = newTunnel.tunnelRemotePort;
this.lastNotification = this.notificationService.prompt(Severity.Info, this.basicMessage(newTunnel) + this.linkMessage(), [this.openChoice(newTunnel)], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } });
this.lastNotification.onDidClose(() => {
this.lastNotification = undefined;
this.lastShownPort = undefined;
});
}
};
}
}
class WindowsAutomaticPortForwarding extends Disposable {
......@@ -310,10 +356,11 @@ class WindowsAutomaticPortForwarding extends Disposable {
readonly openerService: IOpenerService,
private readonly remoteExplorerService: IRemoteExplorerService,
private readonly configurationService: IConfigurationService,
private readonly debugService: IDebugService
private readonly debugService: IDebugService,
readonly tunnelService: ITunnelService
) {
super();
this.notifier = new ForwardedPortNotifier(notificationService, remoteExplorerService, openerService);
this.notifier = new ForwardedPortNotifier(notificationService, remoteExplorerService, openerService, tunnelService);
this._register(configurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) {
this.tryStartStopUrlFinder();
......@@ -372,10 +419,11 @@ class LinuxAutomaticPortForwarding extends Disposable {
private readonly configurationService: IConfigurationService,
readonly remoteExplorerService: IRemoteExplorerService,
readonly notificationService: INotificationService,
readonly openerService: IOpenerService
readonly openerService: IOpenerService,
readonly tunnelService: ITunnelService
) {
super();
this.notifier = new ForwardedPortNotifier(notificationService, remoteExplorerService, openerService);
this.notifier = new ForwardedPortNotifier(notificationService, remoteExplorerService, openerService, tunnelService);
this._register(configurationService.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) {
await this.startStopCandidateListener();
......
......@@ -37,7 +37,7 @@ import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane';
import { URI } from 'vs/base/common/uri';
import { RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { isPortPrivileged, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
......@@ -312,14 +312,14 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
validationOptions: {
validation: (value) => {
const message = editableData.validationMessage(value);
if (!message || message.severity !== Severity.Error) {
if (!message) {
return null;
}
return {
content: message.content,
formatContent: true,
type: MessageType.ERROR
type: message.severity === Severity.Error ? MessageType.ERROR : MessageType.INFO
};
}
},
......@@ -763,17 +763,6 @@ export class TunnelPanelDescriptor implements IViewDescriptor {
}
}
function validationMessage(validationString: string | null): { content: string, severity: Severity } | null {
if (!validationString) {
return null;
}
return {
content: validationString,
severity: Severity.Error
};
}
namespace LabelTunnelAction {
export const ID = 'remote.tunnel.label';
export const LABEL = nls.localize('remote.tunnel.label', "Set Label");
......@@ -803,6 +792,7 @@ namespace LabelTunnelAction {
const invalidPortString: string = nls.localize('remote.tunnelsView.portNumberValid', "Forwarded port is invalid.");
const maxPortNumber: number = 65536;
const invalidPortNumberString: string = nls.localize('remote.tunnelsView.portNumberToHigh', "Port number must be \u2265 0 and < {0}.", maxPortNumber);
const requiresSudoString: string = nls.localize('remote.tunnelView.inlineElevationMessage', "Requires Sudo");
export namespace ForwardPortAction {
export const INLINE_ID = 'remote.tunnel.forwardInline';
......@@ -811,12 +801,14 @@ export namespace ForwardPortAction {
export const TREEITEM_LABEL = nls.localize('remote.tunnel.forwardItem', "Forward Port");
const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000).");
function validateInput(value: string): string | null {
function validateInput(value: string, canElevate: boolean): { content: string, severity: Severity } | null {
const parsed = parseAddress(value);
if (!parsed) {
return invalidPortString;
return { content: invalidPortString, severity: Severity.Error };
} else if (parsed.port >= maxPortNumber) {
return invalidPortNumberString;
return { content: invalidPortNumberString, severity: Severity.Error };
} else if (canElevate && isPortPrivileged(parsed.port)) {
return { content: requiresSudoString, severity: Severity.Info };
}
return null;
}
......@@ -831,6 +823,7 @@ export namespace ForwardPortAction {
return async (accessor, arg) => {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
const notificationService = accessor.get(INotificationService);
const tunnelService = accessor.get(ITunnelService);
if (arg instanceof TunnelItem) {
remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }).then(tunnel => error(notificationService, tunnel, arg.remoteHost, arg.remotePort));
} else {
......@@ -838,11 +831,11 @@ export namespace ForwardPortAction {
onFinish: async (value, success) => {
let parsed: { host: string, port: number } | undefined;
if (success && (parsed = parseAddress(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
remoteExplorerService.forward({ host: parsed.host, port: parsed.port }, undefined, undefined, undefined, true).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
}
remoteExplorerService.setEditable(undefined, null);
},
validationMessage: (value) => validationMessage(validateInput(value)),
validationMessage: (value) => validateInput(value, tunnelService.canElevate),
placeholder: forwardPrompt
});
}
......@@ -855,14 +848,15 @@ export namespace ForwardPortAction {
const notificationService = accessor.get(INotificationService);
const viewsService = accessor.get(IViewsService);
const quickInputService = accessor.get(IQuickInputService);
const tunnelService = accessor.get(ITunnelService);
await viewsService.openView(TunnelPanel.ID, true);
const value = await quickInputService.input({
prompt: forwardPrompt,
validateInput: (value) => Promise.resolve(validateInput(value))
validateInput: (value) => Promise.resolve(validateInput(value, tunnelService.canElevate)?.content)
});
let parsed: { host: string, port: number } | undefined;
if (value && (parsed = parseAddress(value))) {
remoteExplorerService.forward({ host: parsed.host, port: parsed.port }).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
remoteExplorerService.forward({ host: parsed.host, port: parsed.port }, undefined, undefined, undefined, true).then(tunnel => error(notificationService, tunnel, parsed!.host, parsed!.port));
}
};
}
......@@ -1039,11 +1033,13 @@ namespace ChangeLocalPortAction {
export const ID = 'remote.tunnel.changeLocalPort';
export const LABEL = nls.localize('remote.tunnel.changeLocalPort', "Change Local Port");
function validateInput(value: string): string | null {
function validateInput(value: string, canElevate: boolean): { content: string, severity: Severity } | null {
if (!value.match(/^[0-9]+$/)) {
return invalidPortString;
return { content: invalidPortString, severity: Severity.Error };
} else if (Number(value) >= maxPortNumber) {
return invalidPortNumberString;
return { content: invalidPortNumberString, severity: Severity.Error };
} else if (canElevate && isPortPrivileged(Number(value))) {
return { content: requiresSudoString, severity: Severity.Info };
}
return null;
}
......@@ -1052,6 +1048,7 @@ namespace ChangeLocalPortAction {
return async (accessor, arg) => {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
const notificationService = accessor.get(INotificationService);
const tunnelService = accessor.get(ITunnelService);
const context = (arg !== undefined || arg instanceof TunnelItem) ? arg : accessor.get(IContextKeyService).getContextKeyValue(TunnelViewSelectionKeyName);
if (context instanceof TunnelItem) {
remoteExplorerService.setEditable(context, {
......@@ -1060,13 +1057,13 @@ namespace ChangeLocalPortAction {
if (success) {
await remoteExplorerService.close({ host: context.remoteHost, port: context.remotePort });
const numberValue = Number(value);
const newForward = await remoteExplorerService.forward({ host: context.remoteHost, port: context.remotePort }, numberValue, context.name);
const newForward = await remoteExplorerService.forward({ host: context.remoteHost, port: context.remotePort }, numberValue, context.name, undefined, true);
if (newForward && newForward.tunnelLocalPort !== numberValue) {
notificationService.warn(nls.localize('remote.tunnel.changeLocalPortNumber', "The local port {0} is not available. Port number {1} has been used instead", value, newForward.tunnelLocalPort ?? newForward.localAddress));
}
}
},
validationMessage: (value) => validationMessage(validateInput(value)),
validationMessage: (value) => validateInput(value, tunnelService.canElevate),
placeholder: nls.localize('remote.tunnelsView.changePort', "New local port")
});
}
......
......@@ -42,7 +42,7 @@ export class TunnelFactoryContribution extends Disposable implements IWorkbenchC
});
});
}
}));
}, environmentService.options?.tunnelProvider?.features ?? { elevation: false }));
remoteExplorerService.setTunnelInformation(undefined);
}
}
......
......@@ -559,6 +559,7 @@ class SimpleTunnelService implements ITunnelService {
declare readonly _serviceBrand: undefined;
tunnels: Promise<readonly RemoteTunnel[]> = Promise.resolve([]);
canElevate: boolean = false;
onTunnelOpened = Event.None;
onTunnelClosed = Event.None;
......
......@@ -201,7 +201,7 @@ export class TunnelModel extends Disposable {
}
}
async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string): Promise<RemoteTunnel | void> {
async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean): Promise<RemoteTunnel | void> {
const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, remote.host, remote.port);
if (!existingTunnel) {
const authority = this.environmentService.remoteAuthority;
......@@ -209,7 +209,7 @@ export class TunnelModel extends Disposable {
getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; }
} : undefined;
const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local);
const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local, elevateIfNeeded);
if (tunnel && tunnel.localAddress) {
const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), remote.host, remote.port);
const newForward: Tunnel = {
......@@ -362,7 +362,7 @@ export interface IRemoteExplorerService {
onDidChangeEditable: Event<ITunnelItem | undefined>;
setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void;
getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined;
forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string): Promise<RemoteTunnel | void>;
forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean): Promise<RemoteTunnel | void>;
close(remote: { host: string, port: number }): Promise<void>;
setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void;
setCandidateFilter(filter: ((candidates: CandidatePort[]) => Promise<CandidatePort[]>) | undefined): IDisposable;
......@@ -415,8 +415,8 @@ class RemoteExplorerService implements IRemoteExplorerService {
return this._tunnelModel;
}
forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string): Promise<RemoteTunnel | void> {
return this.tunnelModel.forward(remote, local, name, source);
forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean): Promise<RemoteTunnel | void> {
return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded);
}
close(remote: { host: string, port: number }): Promise<void> {
......
......@@ -19,6 +19,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IProductConfiguration } from 'vs/platform/product/common/productService';
import { mark } from 'vs/base/common/performance';
import { ICredentialsProvider } from 'vs/workbench/services/credentials/common/credentials';
import { TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel';
interface IResourceUriProvider {
(uri: URI): URI;
......@@ -46,9 +47,14 @@ interface ITunnelProvider {
tunnelFactory?: ITunnelFactory;
/**
* Support for filtering candidate ports
* Support for filtering candidate ports.
*/
showPortCandidate?: IShowPortCandidate;
/**
* The features that the tunnel provider supports.
*/
features?: TunnelProviderFeatures;
}
interface ITunnelFactory {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册