mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge vscode 1.67 (#20883)
* Fix initial build breaks from 1.67 merge (#2514) * Update yarn lock files * Update build scripts * Fix tsconfig * Build breaks * WIP * Update yarn lock files * Misc breaks * Updates to package.json * Breaks * Update yarn * Fix breaks * Breaks * Build breaks * Breaks * Breaks * Breaks * Breaks * Breaks * Missing file * Breaks * Breaks * Breaks * Breaks * Breaks * Fix several runtime breaks (#2515) * Missing files * Runtime breaks * Fix proxy ordering issue * Remove commented code * Fix breaks with opening query editor * Fix post merge break * Updates related to setup build and other breaks (#2516) * Fix bundle build issues * Update distro * Fix distro merge and update build JS files * Disable pipeline steps * Remove stats call * Update license name * Make new RPM dependencies a warning * Fix extension manager version checks * Update JS file * Fix a few runtime breaks * Fixes * Fix runtime issues * Fix build breaks * Update notebook tests (part 1) * Fix broken tests * Linting errors * Fix hygiene * Disable lint rules * Bump distro * Turn off smoke tests * Disable integration tests * Remove failing "activate" test * Remove failed test assertion * Disable other broken test * Disable query history tests * Disable extension unit tests * Disable failing tasks
This commit is contained in:
400
src/vs/platform/tunnel/common/tunnel.ts
Normal file
400
src/vs/platform/tunnel/common/tunnel.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isWindows, OperatingSystem } 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';
|
||||
import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { TunnelPrivacy } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
|
||||
export const ITunnelService = createDecorator<ITunnelService>('tunnelService');
|
||||
export const ISharedTunnelsService = createDecorator<ISharedTunnelsService>('sharedTunnelsService');
|
||||
|
||||
export interface RemoteTunnel {
|
||||
readonly tunnelRemotePort: number;
|
||||
readonly tunnelRemoteHost: string;
|
||||
readonly tunnelLocalPort?: number;
|
||||
readonly localAddress: string;
|
||||
readonly privacy: string;
|
||||
readonly protocol?: string;
|
||||
dispose(silent?: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface TunnelOptions {
|
||||
remoteAddress: { port: number; host: string };
|
||||
localAddressPort?: number;
|
||||
label?: string;
|
||||
public?: boolean;
|
||||
privacy?: string;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
export enum TunnelProtocol {
|
||||
Http = 'http',
|
||||
Https = 'https'
|
||||
}
|
||||
|
||||
export enum TunnelPrivacyId {
|
||||
ConstantPrivate = 'constantPrivate', // private, and changing is unsupported
|
||||
Private = 'private',
|
||||
Public = 'public'
|
||||
}
|
||||
|
||||
export interface TunnelCreationOptions {
|
||||
elevationRequired?: boolean;
|
||||
}
|
||||
|
||||
export interface TunnelProviderFeatures {
|
||||
elevation: boolean;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public?: boolean;
|
||||
privacyOptions: TunnelPrivacy[];
|
||||
}
|
||||
|
||||
export interface ITunnelProvider {
|
||||
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel | undefined> | undefined;
|
||||
}
|
||||
|
||||
export enum ProvidedOnAutoForward {
|
||||
Notify = 1,
|
||||
OpenBrowser = 2,
|
||||
OpenPreview = 3,
|
||||
Silent = 4,
|
||||
Ignore = 5,
|
||||
OpenBrowserOnce = 6
|
||||
}
|
||||
|
||||
export interface ProvidedPortAttributes {
|
||||
port: number;
|
||||
autoForwardAction: ProvidedOnAutoForward;
|
||||
}
|
||||
|
||||
export interface PortAttributesProvider {
|
||||
providePortAttributes(ports: number[], pid: number | undefined, commandLine: string | undefined, token: CancellationToken): Promise<ProvidedPortAttributes[]>;
|
||||
}
|
||||
|
||||
export interface ITunnel {
|
||||
remoteAddress: { port: number; host: string };
|
||||
|
||||
/**
|
||||
* The complete local address(ex. localhost:1234)
|
||||
*/
|
||||
localAddress: string;
|
||||
|
||||
/**
|
||||
* @deprecated Use privacy instead
|
||||
*/
|
||||
public?: boolean;
|
||||
|
||||
privacy?: string;
|
||||
|
||||
protocol?: string;
|
||||
|
||||
/**
|
||||
* Implementers of Tunnel should fire onDidDispose when dispose is called.
|
||||
*/
|
||||
onDidDispose: Event<void>;
|
||||
|
||||
dispose(): Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface ISharedTunnelsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
|
||||
}
|
||||
|
||||
export interface ITunnelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly tunnels: Promise<readonly RemoteTunnel[]>;
|
||||
readonly canChangePrivacy: boolean;
|
||||
readonly privacyOptions: TunnelPrivacy[];
|
||||
readonly onTunnelOpened: Event<RemoteTunnel>;
|
||||
readonly onTunnelClosed: Event<{ host: string; port: number }>;
|
||||
readonly canElevate: boolean;
|
||||
readonly hasTunnelProvider: boolean;
|
||||
readonly onAddedTunnelProvider: Event<void>;
|
||||
|
||||
canTunnel(uri: URI): boolean;
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
|
||||
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
|
||||
setTunnelFeatures(features: TunnelProviderFeatures): void;
|
||||
}
|
||||
|
||||
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string; port: number } | undefined {
|
||||
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
|
||||
return undefined;
|
||||
}
|
||||
const localhostMatch = /^(localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)$/.exec(uri.authority);
|
||||
if (!localhostMatch) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
address: localhostMatch[1],
|
||||
port: +localhostMatch[2],
|
||||
};
|
||||
}
|
||||
|
||||
export const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1'];
|
||||
export function isLocalhost(host: string): boolean {
|
||||
return LOCALHOST_ADDRESSES.indexOf(host) >= 0;
|
||||
}
|
||||
|
||||
export const ALL_INTERFACES_ADDRESSES = ['0.0.0.0', '0:0:0:0:0:0:0:0', '::'];
|
||||
export function isAllInterfaces(host: string): boolean {
|
||||
return ALL_INTERFACES_ADDRESSES.indexOf(host) >= 0;
|
||||
}
|
||||
|
||||
export function isPortPrivileged(port: number, os?: OperatingSystem): boolean {
|
||||
if (os) {
|
||||
return os !== OperatingSystem.Windows && (port < 1024);
|
||||
} else {
|
||||
return !isWindows && (port < 1024);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private _onAddedTunnelProvider: Emitter<void> = new Emitter();
|
||||
public onAddedTunnelProvider: Event<void> = this._onAddedTunnelProvider.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;
|
||||
private _privacyOptions: TunnelPrivacy[] = [];
|
||||
|
||||
public constructor(
|
||||
@ILogService protected readonly logService: ILogService
|
||||
) { }
|
||||
|
||||
get hasTunnelProvider(): boolean {
|
||||
return !!this._tunnelProvider;
|
||||
}
|
||||
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
|
||||
this._tunnelProvider = provider;
|
||||
if (!provider) {
|
||||
// clear features
|
||||
this._canElevate = false;
|
||||
this._privacyOptions = [];
|
||||
this._onAddedTunnelProvider.fire();
|
||||
return {
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
|
||||
this._onAddedTunnelProvider.fire();
|
||||
return {
|
||||
dispose: () => {
|
||||
this._tunnelProvider = undefined;
|
||||
this._canElevate = false;
|
||||
this._privacyOptions = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setTunnelFeatures(features: TunnelProviderFeatures): void {
|
||||
this._canElevate = features.elevation;
|
||||
this._privacyOptions = features.privacyOptions;
|
||||
}
|
||||
|
||||
public get canElevate(): boolean {
|
||||
return this._canElevate;
|
||||
}
|
||||
|
||||
public get canChangePrivacy() {
|
||||
return this._privacyOptions.length > 0;
|
||||
}
|
||||
|
||||
public get privacyOptions() {
|
||||
return this._privacyOptions;
|
||||
}
|
||||
|
||||
public get tunnels(): Promise<readonly RemoteTunnel[]> {
|
||||
return this.getTunnels();
|
||||
}
|
||||
|
||||
private async getTunnels(): Promise<readonly RemoteTunnel[]> {
|
||||
const tunnels: RemoteTunnel[] = [];
|
||||
const tunnelArray = Array.from(this._tunnels.values());
|
||||
for (let portMap of tunnelArray) {
|
||||
const portArray = Array.from(portMap.values());
|
||||
for (let x of portArray) {
|
||||
const tunnelValue = await x.value;
|
||||
if (tunnelValue) {
|
||||
tunnels.push(tunnelValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tunnels;
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
for (const portMap of this._tunnels.values()) {
|
||||
for (const { value } of portMap.values()) {
|
||||
await value.then(tunnel => tunnel?.dispose());
|
||||
}
|
||||
portMap.clear();
|
||||
}
|
||||
this._tunnels.clear();
|
||||
}
|
||||
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, privacy: string = TunnelPrivacyId.Private, protocol?: string): Promise<RemoteTunnel | undefined> | undefined {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
|
||||
if (!addressProvider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!remoteHost) {
|
||||
remoteHost = 'localhost';
|
||||
}
|
||||
|
||||
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol);
|
||||
if (!resolvedTunnel) {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) Tunnel was not created.`);
|
||||
return resolvedTunnel;
|
||||
}
|
||||
|
||||
return resolvedTunnel.then(tunnel => {
|
||||
if (!tunnel) {
|
||||
this.logService.trace('ForwardedPorts: (TunnelService) New tunnel is undefined.');
|
||||
this.removeEmptyTunnelFromMap(remoteHost!, remotePort);
|
||||
return undefined;
|
||||
}
|
||||
this.logService.trace('ForwardedPorts: (TunnelService) New tunnel established.');
|
||||
const newTunnel = this.makeTunnel(tunnel);
|
||||
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
|
||||
this.logService.warn('ForwardedPorts: (TunnelService) 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,
|
||||
privacy: tunnel.privacy,
|
||||
protocol: tunnel.protocol,
|
||||
dispose: async () => {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) dispose request for ${tunnel.tunnelRemoteHost}:${tunnel.tunnelRemotePort} `);
|
||||
const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost);
|
||||
if (existingHost) {
|
||||
const existing = existingHost.get(tunnel.tunnelRemotePort);
|
||||
if (existing) {
|
||||
existing.refcount--;
|
||||
await this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number; readonly value: Promise<RemoteTunnel | undefined> }): Promise<void> {
|
||||
if (tunnel.refcount <= 0) {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) Tunnel is being disposed ${remoteHost}:${remotePort}.`);
|
||||
const disposePromise: Promise<void> = tunnel.value.then(async (tunnel) => {
|
||||
if (tunnel) {
|
||||
await 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> {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) close request for ${remoteHost}:${remotePort} `);
|
||||
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 | undefined>) {
|
||||
if (!this._tunnels.has(remoteHost)) {
|
||||
this._tunnels.set(remoteHost, new Map());
|
||||
}
|
||||
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
|
||||
}
|
||||
|
||||
private async removeEmptyTunnelFromMap(remoteHost: string, remotePort: number) {
|
||||
const hostMap = this._tunnels.get(remoteHost);
|
||||
if (hostMap) {
|
||||
const tunnel = hostMap.get(remotePort);
|
||||
const tunnelResult = await tunnel;
|
||||
if (!tunnelResult) {
|
||||
hostMap.delete(remotePort);
|
||||
}
|
||||
if (hostMap.size === 0) {
|
||||
this._tunnels.delete(remoteHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number; readonly value: Promise<RemoteTunnel | undefined> } | undefined {
|
||||
let hosts = [remoteHost];
|
||||
// Order matters. We want the original host to be first.
|
||||
if (isLocalhost(remoteHost)) {
|
||||
hosts.push(...LOCALHOST_ADDRESSES);
|
||||
// For localhost, we add the all interfaces hosts because if the tunnel is already available at all interfaces,
|
||||
// then of course it is available at localhost.
|
||||
hosts.push(...ALL_INTERFACES_ADDRESSES);
|
||||
} else if (isAllInterfaces(remoteHost)) {
|
||||
hosts.push(...ALL_INTERFACES_ADDRESSES);
|
||||
}
|
||||
|
||||
const existingPortMaps = hosts.map(host => this._tunnels.get(host));
|
||||
for (const map of existingPortMaps) {
|
||||
const existingTunnel = map?.get(remotePort);
|
||||
if (existingTunnel) {
|
||||
return existingTunnel;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
canTunnel(uri: URI): boolean {
|
||||
return !!extractLocalHostUriMetaDataForPortMapping(uri);
|
||||
}
|
||||
|
||||
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined;
|
||||
|
||||
protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel with provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
|
||||
|
||||
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
|
||||
const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false };
|
||||
const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, privacy, public: privacy !== TunnelPrivacyId.Private, protocol };
|
||||
const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo);
|
||||
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created by provider.');
|
||||
if (tunnel) {
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
}
|
||||
return tunnel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
123
src/vs/platform/tunnel/node/sharedProcessTunnelService.ts
Normal file
123
src/vs/platform/tunnel/node/sharedProcessTunnelService.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ISharedProcessTunnel, ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedProcessTunnelService';
|
||||
import { ISharedTunnelsService, RemoteTunnel } from 'vs/platform/tunnel/common/tunnel';
|
||||
import { IAddress, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { DeferredPromise } from 'vs/base/common/async';
|
||||
|
||||
class TunnelData extends Disposable implements IAddressProvider {
|
||||
|
||||
private _address: IAddress | null;
|
||||
private _addressPromise: DeferredPromise<IAddress> | null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._address = null;
|
||||
this._addressPromise = null;
|
||||
}
|
||||
|
||||
async getAddress(): Promise<IAddress> {
|
||||
if (this._address) {
|
||||
// address is resolved
|
||||
return this._address;
|
||||
}
|
||||
if (!this._addressPromise) {
|
||||
this._addressPromise = new DeferredPromise<IAddress>();
|
||||
}
|
||||
return this._addressPromise.p;
|
||||
}
|
||||
|
||||
setAddress(address: IAddress): void {
|
||||
this._address = address;
|
||||
if (this._addressPromise) {
|
||||
this._addressPromise.complete(address);
|
||||
this._addressPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
setTunnel(tunnel: RemoteTunnel): void {
|
||||
this._register(tunnel);
|
||||
}
|
||||
}
|
||||
|
||||
export class SharedProcessTunnelService extends Disposable implements ISharedProcessTunnelService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private static _lastId = 0;
|
||||
|
||||
private readonly _tunnels: Map<string, TunnelData> = new Map<string, TunnelData>();
|
||||
private readonly _disposedTunnels: Set<string> = new Set<string>();
|
||||
|
||||
constructor(
|
||||
@ISharedTunnelsService private readonly _tunnelService: ISharedTunnelsService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
this._tunnels.forEach((tunnel) => tunnel.dispose());
|
||||
}
|
||||
|
||||
async createTunnel(): Promise<{ id: string }> {
|
||||
const id = String(++SharedProcessTunnelService._lastId);
|
||||
return { id };
|
||||
}
|
||||
|
||||
async startTunnel(authority: string, id: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort: number | undefined, elevateIfNeeded: boolean | undefined): Promise<ISharedProcessTunnel> {
|
||||
const tunnelData = new TunnelData();
|
||||
|
||||
const tunnel = await Promise.resolve(this._tunnelService.openTunnel(authority, tunnelData, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort, elevateIfNeeded));
|
||||
if (!tunnel) {
|
||||
this._logService.info(`[SharedProcessTunnelService] Could not create a tunnel to ${tunnelRemoteHost}:${tunnelRemotePort} (remote).`);
|
||||
tunnelData.dispose();
|
||||
throw new Error(`Could not create tunnel`);
|
||||
}
|
||||
|
||||
if (this._disposedTunnels.has(id)) {
|
||||
// This tunnel was disposed in the meantime
|
||||
this._disposedTunnels.delete(id);
|
||||
tunnelData.dispose();
|
||||
await tunnel.dispose();
|
||||
throw canceled();
|
||||
}
|
||||
|
||||
tunnelData.setTunnel(tunnel);
|
||||
this._tunnels.set(id, tunnelData);
|
||||
|
||||
this._logService.info(`[SharedProcessTunnelService] Created tunnel ${id}: ${tunnel.localAddress} (local) to ${tunnelRemoteHost}:${tunnelRemotePort} (remote).`);
|
||||
const result: ISharedProcessTunnel = {
|
||||
tunnelLocalPort: tunnel.tunnelLocalPort,
|
||||
localAddress: tunnel.localAddress
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
async setAddress(id: string, address: IAddress): Promise<void> {
|
||||
const tunnel = this._tunnels.get(id);
|
||||
if (!tunnel) {
|
||||
return;
|
||||
}
|
||||
tunnel.setAddress(address);
|
||||
}
|
||||
|
||||
async destroyTunnel(id: string): Promise<void> {
|
||||
const tunnel = this._tunnels.get(id);
|
||||
if (tunnel) {
|
||||
this._logService.info(`[SharedProcessTunnelService] Disposing tunnel ${id}.`);
|
||||
this._tunnels.delete(id);
|
||||
await tunnel.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// Looks like this tunnel is still starting, mark the id as disposed
|
||||
this._disposedTunnels.add(id);
|
||||
}
|
||||
}
|
||||
237
src/vs/platform/tunnel/node/tunnelService.ts
Normal file
237
src/vs/platform/tunnel/node/tunnelService.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
import { BROWSER_RESTRICTED_PORTS, findFreePortFaster } from 'vs/base/node/ports';
|
||||
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
|
||||
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { connectRemoteAgentTunnel, IAddressProvider, IConnectionOptions, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { AbstractTunnelService, isAllInterfaces, ISharedTunnelsService as ISharedTunnelsService, isLocalhost, ITunnelService, RemoteTunnel, TunnelPrivacyId } from 'vs/platform/tunnel/common/tunnel';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
|
||||
async function createRemoteTunnel(options: IConnectionOptions, defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
|
||||
let readyTunnel: NodeRemoteTunnel | undefined;
|
||||
for (let attempts = 3; attempts; attempts--) {
|
||||
if (readyTunnel) {
|
||||
readyTunnel.dispose();
|
||||
}
|
||||
const tunnel = new NodeRemoteTunnel(options, defaultTunnelHost, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
|
||||
readyTunnel = await tunnel.waitForReady();
|
||||
if ((tunnelLocalPort && BROWSER_RESTRICTED_PORTS[tunnelLocalPort]) || !BROWSER_RESTRICTED_PORTS[readyTunnel.tunnelLocalPort]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return readyTunnel!;
|
||||
}
|
||||
|
||||
class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
|
||||
|
||||
public readonly tunnelRemotePort: number;
|
||||
public tunnelLocalPort!: number;
|
||||
public tunnelRemoteHost: string;
|
||||
public localAddress!: string;
|
||||
public readonly privacy = TunnelPrivacyId.Private;
|
||||
|
||||
private readonly _options: IConnectionOptions;
|
||||
private readonly _server: net.Server;
|
||||
private readonly _barrier: Barrier;
|
||||
|
||||
private readonly _listeningListener: () => void;
|
||||
private readonly _connectionListener: (socket: net.Socket) => void;
|
||||
private readonly _errorListener: () => void;
|
||||
|
||||
private readonly _socketsDispose: Map<string, () => void> = new Map();
|
||||
|
||||
constructor(options: IConnectionOptions, private readonly defaultTunnelHost: string, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
|
||||
super();
|
||||
this._options = options;
|
||||
this._server = net.createServer();
|
||||
this._barrier = new Barrier();
|
||||
|
||||
this._listeningListener = () => this._barrier.open();
|
||||
this._server.on('listening', this._listeningListener);
|
||||
|
||||
this._connectionListener = (socket) => this._onConnection(socket);
|
||||
this._server.on('connection', this._connectionListener);
|
||||
|
||||
// If there is no error listener and there is an error it will crash the whole window
|
||||
this._errorListener = () => { };
|
||||
this._server.on('error', this._errorListener);
|
||||
|
||||
this.tunnelRemotePort = tunnelRemotePort;
|
||||
this.tunnelRemoteHost = tunnelRemoteHost;
|
||||
}
|
||||
|
||||
public override async dispose(): Promise<void> {
|
||||
super.dispose();
|
||||
this._server.removeListener('listening', this._listeningListener);
|
||||
this._server.removeListener('connection', this._connectionListener);
|
||||
this._server.removeListener('error', this._errorListener);
|
||||
this._server.close();
|
||||
const disposers = Array.from(this._socketsDispose.values());
|
||||
disposers.forEach(disposer => {
|
||||
disposer();
|
||||
});
|
||||
}
|
||||
|
||||
public async waitForReady(): Promise<this> {
|
||||
// try to get the same port number as the remote port number...
|
||||
let localPort = await findFreePortFaster(this.suggestedLocalPort ?? this.tunnelRemotePort, 2, 1000);
|
||||
|
||||
// if that fails, the method above returns 0, which works out fine below...
|
||||
let address: string | net.AddressInfo | null = null;
|
||||
this._server.listen(localPort, this.defaultTunnelHost);
|
||||
await this._barrier.wait();
|
||||
address = <net.AddressInfo>this._server.address();
|
||||
|
||||
// It is possible for findFreePortFaster to return a port that there is already a server listening on. This causes the previous listen call to error out.
|
||||
if (!address) {
|
||||
localPort = 0;
|
||||
this._server.listen(localPort, this.defaultTunnelHost);
|
||||
await this._barrier.wait();
|
||||
address = <net.AddressInfo>this._server.address();
|
||||
}
|
||||
|
||||
this.tunnelLocalPort = address.port;
|
||||
this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
|
||||
return this;
|
||||
}
|
||||
|
||||
private async _onConnection(localSocket: net.Socket): Promise<void> {
|
||||
// pause reading on the socket until we have a chance to forward its data
|
||||
localSocket.pause();
|
||||
|
||||
const tunnelRemoteHost = (isLocalhost(this.tunnelRemoteHost) || isAllInterfaces(this.tunnelRemoteHost)) ? 'localhost' : this.tunnelRemoteHost;
|
||||
const protocol = await connectRemoteAgentTunnel(this._options, tunnelRemoteHost, this.tunnelRemotePort);
|
||||
const remoteSocket = (<NodeSocket>protocol.getSocket()).socket;
|
||||
const dataChunk = protocol.readEntireBuffer();
|
||||
protocol.dispose();
|
||||
|
||||
if (dataChunk.byteLength > 0) {
|
||||
localSocket.write(dataChunk.buffer);
|
||||
}
|
||||
|
||||
localSocket.on('end', () => {
|
||||
if (localSocket.localAddress) {
|
||||
this._socketsDispose.delete(localSocket.localAddress);
|
||||
}
|
||||
remoteSocket.end();
|
||||
});
|
||||
localSocket.on('close', () => remoteSocket.end());
|
||||
localSocket.on('error', () => {
|
||||
if (localSocket.localAddress) {
|
||||
this._socketsDispose.delete(localSocket.localAddress);
|
||||
}
|
||||
remoteSocket.destroy();
|
||||
});
|
||||
|
||||
remoteSocket.on('end', () => localSocket.end());
|
||||
remoteSocket.on('close', () => localSocket.end());
|
||||
remoteSocket.on('error', () => {
|
||||
localSocket.destroy();
|
||||
});
|
||||
|
||||
localSocket.pipe(remoteSocket);
|
||||
remoteSocket.pipe(localSocket);
|
||||
if (localSocket.localAddress) {
|
||||
this._socketsDispose.set(localSocket.localAddress, () => {
|
||||
// Need to end instead of unpipe, otherwise whatever is connected locally could end up "stuck" with whatever state it had until manually exited.
|
||||
localSocket.end();
|
||||
remoteSocket.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseTunnelService extends AbstractTunnelService {
|
||||
public constructor(
|
||||
private readonly socketFactory: ISocketFactory,
|
||||
@ILogService logService: ILogService,
|
||||
@ISignService private readonly signService: ISignService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
private get defaultTunnelHost(): string {
|
||||
const settingValue = this.configurationService.getValue('remote.localPortHost');
|
||||
return (!settingValue || settingValue === 'localhost') ? '127.0.0.1' : '0.0.0.0';
|
||||
}
|
||||
|
||||
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, privacy: string, protocol?: string): Promise<RemoteTunnel | undefined> | undefined {
|
||||
const existing = this.getTunnelFromMap(remoteHost, remotePort);
|
||||
if (existing) {
|
||||
++existing.refcount;
|
||||
return existing.value;
|
||||
}
|
||||
|
||||
if (this._tunnelProvider) {
|
||||
return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol);
|
||||
} else {
|
||||
this.logService.trace(`ForwardedPorts: (TunnelService) Creating tunnel without provider ${remoteHost}:${remotePort} on local port ${localPort}.`);
|
||||
const options: IConnectionOptions = {
|
||||
commit: this.productService.commit,
|
||||
socketFactory: this.socketFactory,
|
||||
addressProvider,
|
||||
signService: this.signService,
|
||||
logService: this.logService,
|
||||
ipcLogger: null
|
||||
};
|
||||
|
||||
const tunnel = createRemoteTunnel(options, this.defaultTunnelHost, remoteHost, remotePort, localPort);
|
||||
this.logService.trace('ForwardedPorts: (TunnelService) Tunnel created without provider.');
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
return tunnel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TunnelService extends BaseTunnelService {
|
||||
public constructor(
|
||||
@ILogService logService: ILogService,
|
||||
@ISignService signService: ISignService,
|
||||
@IProductService productService: IProductService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(nodeSocketFactory, logService, signService, productService, configurationService);
|
||||
}
|
||||
}
|
||||
|
||||
export class SharedTunnelsService extends Disposable implements ISharedTunnelsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly _tunnelServices: Map<string, ITunnelService> = new Map();
|
||||
|
||||
public constructor(
|
||||
@ILogService protected readonly logService: ILogService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@ISignService private readonly signService: ISignService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async openTunnel(authority: string, addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, privacy?: string, protocol?: string): Promise<RemoteTunnel | undefined> {
|
||||
this.logService.trace(`ForwardedPorts: (SharedTunnelService) openTunnel request for ${remoteHost}:${remotePort} on local port ${localPort}.`);
|
||||
if (!this._tunnelServices.has(authority)) {
|
||||
const tunnelService = new TunnelService(this.logService, this.signService, this.productService, this.configurationService);
|
||||
this._register(tunnelService);
|
||||
this._tunnelServices.set(authority, tunnelService);
|
||||
tunnelService.onTunnelClosed(async () => {
|
||||
if ((await tunnelService.tunnels).length === 0) {
|
||||
tunnelService.dispose();
|
||||
this._tunnelServices.delete(authority);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this._tunnelServices.get(authority)!.openTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user