mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Merge from vscode 4d91d96e5e121b38d33508cdef17868bab255eae
This commit is contained in:
committed by
AzureDataStudio
parent
a971aee5bd
commit
5e7071e466
@@ -3,41 +3,74 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { RemoteAuthorities } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverService {
|
||||
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
resourceUriProvider: ((uri: URI) => URI) | undefined
|
||||
) {
|
||||
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
|
||||
|
||||
private readonly _cache: Map<string, ResolverResult>;
|
||||
private readonly _connectionTokens: Map<string, string>;
|
||||
|
||||
constructor(resourceUriProvider: ((uri: URI) => URI) | undefined) {
|
||||
super();
|
||||
this._cache = new Map<string, ResolverResult>();
|
||||
this._connectionTokens = new Map<string, string>();
|
||||
if (resourceUriProvider) {
|
||||
RemoteAuthorities.setDelegate(resourceUriProvider);
|
||||
}
|
||||
}
|
||||
|
||||
resolveAuthority(authority: string): Promise<ResolverResult> {
|
||||
async resolveAuthority(authority: string): Promise<ResolverResult> {
|
||||
if (!this._cache.has(authority)) {
|
||||
const result = this._doResolveAuthority(authority);
|
||||
RemoteAuthorities.set(authority, result.authority.host, result.authority.port);
|
||||
this._cache.set(authority, result);
|
||||
this._onDidChangeConnectionData.fire();
|
||||
}
|
||||
return this._cache.get(authority)!;
|
||||
}
|
||||
|
||||
getConnectionData(authority: string): IRemoteConnectionData | null {
|
||||
if (!this._cache.has(authority)) {
|
||||
return null;
|
||||
}
|
||||
const resolverResult = this._cache.get(authority)!;
|
||||
const connectionToken = this._connectionTokens.get(authority);
|
||||
return {
|
||||
host: resolverResult.authority.host,
|
||||
port: resolverResult.authority.port,
|
||||
connectionToken: connectionToken
|
||||
};
|
||||
}
|
||||
|
||||
private _doResolveAuthority(authority: string): ResolverResult {
|
||||
if (authority.indexOf(':') >= 0) {
|
||||
const pieces = authority.split(':');
|
||||
return Promise.resolve(this._createResolvedAuthority(authority, pieces[0], parseInt(pieces[1], 10)));
|
||||
return { authority: { authority, host: pieces[0], port: parseInt(pieces[1], 10) } };
|
||||
}
|
||||
return Promise.resolve(this._createResolvedAuthority(authority, authority, 80));
|
||||
return { authority: { authority, host: authority, port: 80 } };
|
||||
}
|
||||
|
||||
private _createResolvedAuthority(authority: string, host: string, port: number): ResolverResult {
|
||||
RemoteAuthorities.set(authority, host, port);
|
||||
return { authority: { authority, host, port } };
|
||||
_clearResolvedAuthority(authority: string): void {
|
||||
}
|
||||
|
||||
clearResolvedAuthority(authority: string): void {
|
||||
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority) {
|
||||
}
|
||||
|
||||
setResolvedAuthority(resolvedAuthority: ResolvedAuthority) {
|
||||
_setResolvedAuthorityError(authority: string, err: any): void {
|
||||
}
|
||||
|
||||
setResolvedAuthorityError(authority: string, err: any): void {
|
||||
_setAuthorityConnectionToken(authority: string, connectionToken: string): void {
|
||||
this._connectionTokens.set(authority, connectionToken);
|
||||
RemoteAuthorities.setConnectionToken(authority, connectionToken);
|
||||
this._onDidChangeConnectionData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export const IRemoteAuthorityResolverService = createDecorator<IRemoteAuthorityResolverService>('remoteAuthorityResolverService');
|
||||
|
||||
@@ -31,6 +32,12 @@ export interface ResolverResult {
|
||||
tunnelInformation?: TunnelInformation;
|
||||
}
|
||||
|
||||
export interface IRemoteConnectionData {
|
||||
host: string;
|
||||
port: number;
|
||||
connectionToken: string | undefined;
|
||||
}
|
||||
|
||||
export enum RemoteAuthorityResolverErrorCode {
|
||||
Unknown = 'Unknown',
|
||||
NotAvailable = 'NotAvailable',
|
||||
@@ -77,11 +84,15 @@ export class RemoteAuthorityResolverError extends Error {
|
||||
|
||||
export interface IRemoteAuthorityResolverService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeConnectionData: Event<void>;
|
||||
|
||||
resolveAuthority(authority: string): Promise<ResolverResult>;
|
||||
getConnectionData(authority: string): IRemoteConnectionData | null;
|
||||
|
||||
clearResolvedAuthority(authority: string): void;
|
||||
setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void;
|
||||
setResolvedAuthorityError(authority: string, err: any): void;
|
||||
_clearResolvedAuthority(authority: string): void;
|
||||
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void;
|
||||
_setResolvedAuthorityError(authority: string, err: any): void;
|
||||
_setAuthorityConnectionToken(authority: string, connectionToken: string): void;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
* Licensed under the Source EULA. 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');
|
||||
|
||||
@@ -29,13 +31,13 @@ export interface ITunnelProvider {
|
||||
}
|
||||
|
||||
export interface ITunnelService {
|
||||
_serviceBrand: undefined;
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly tunnels: Promise<readonly RemoteTunnel[]>;
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,61 +3,105 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult, ResolvedOptions } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { ResolvedAuthority, IRemoteAuthorityResolverService, ResolverResult, ResolvedOptions, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { RemoteAuthorities } from 'vs/base/common/network';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
class PendingResolveAuthorityRequest {
|
||||
|
||||
public value: ResolverResult | null;
|
||||
|
||||
constructor(
|
||||
public readonly resolve: (value: ResolverResult) => void,
|
||||
public readonly reject: (err: any) => void,
|
||||
private readonly _resolve: (value: ResolverResult) => void,
|
||||
private readonly _reject: (err: any) => void,
|
||||
public readonly promise: Promise<ResolverResult>,
|
||||
) {
|
||||
this.value = null;
|
||||
}
|
||||
|
||||
resolve(value: ResolverResult): void {
|
||||
this.value = value;
|
||||
this._resolve(this.value);
|
||||
}
|
||||
|
||||
reject(err: any): void {
|
||||
this._reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteAuthorityResolverService implements IRemoteAuthorityResolverService {
|
||||
export class RemoteAuthorityResolverService extends Disposable implements IRemoteAuthorityResolverService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _resolveAuthorityRequests: { [authority: string]: PendingResolveAuthorityRequest; };
|
||||
private readonly _onDidChangeConnectionData = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeConnectionData = this._onDidChangeConnectionData.event;
|
||||
|
||||
private readonly _resolveAuthorityRequests: Map<string, PendingResolveAuthorityRequest>;
|
||||
private readonly _connectionTokens: Map<string, string>;
|
||||
|
||||
constructor() {
|
||||
this._resolveAuthorityRequests = Object.create(null);
|
||||
super();
|
||||
this._resolveAuthorityRequests = new Map<string, PendingResolveAuthorityRequest>();
|
||||
this._connectionTokens = new Map<string, string>();
|
||||
}
|
||||
|
||||
resolveAuthority(authority: string): Promise<ResolverResult> {
|
||||
if (!this._resolveAuthorityRequests[authority]) {
|
||||
if (!this._resolveAuthorityRequests.has(authority)) {
|
||||
let resolve: (value: ResolverResult) => void;
|
||||
let reject: (err: any) => void;
|
||||
let promise = new Promise<ResolverResult>((_resolve, _reject) => {
|
||||
const promise = new Promise<ResolverResult>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
this._resolveAuthorityRequests[authority] = new PendingResolveAuthorityRequest(resolve!, reject!, promise);
|
||||
this._resolveAuthorityRequests.set(authority, new PendingResolveAuthorityRequest(resolve!, reject!, promise));
|
||||
}
|
||||
return this._resolveAuthorityRequests[authority].promise;
|
||||
return this._resolveAuthorityRequests.get(authority)!.promise;
|
||||
}
|
||||
|
||||
clearResolvedAuthority(authority: string): void {
|
||||
if (this._resolveAuthorityRequests[authority]) {
|
||||
this._resolveAuthorityRequests[authority].reject(errors.canceled());
|
||||
delete this._resolveAuthorityRequests[authority];
|
||||
getConnectionData(authority: string): IRemoteConnectionData | null {
|
||||
if (!this._resolveAuthorityRequests.has(authority)) {
|
||||
return null;
|
||||
}
|
||||
const request = this._resolveAuthorityRequests.get(authority)!;
|
||||
if (!request.value) {
|
||||
return null;
|
||||
}
|
||||
const connectionToken = this._connectionTokens.get(authority);
|
||||
return {
|
||||
host: request.value.authority.host,
|
||||
port: request.value.authority.port,
|
||||
connectionToken: connectionToken
|
||||
};
|
||||
}
|
||||
|
||||
_clearResolvedAuthority(authority: string): void {
|
||||
if (this._resolveAuthorityRequests.has(authority)) {
|
||||
this._resolveAuthorityRequests.get(authority)!.reject(errors.canceled());
|
||||
this._resolveAuthorityRequests.delete(authority);
|
||||
}
|
||||
}
|
||||
|
||||
setResolvedAuthority(resolvedAuthority: ResolvedAuthority, options?: ResolvedOptions) {
|
||||
if (this._resolveAuthorityRequests[resolvedAuthority.authority]) {
|
||||
let request = this._resolveAuthorityRequests[resolvedAuthority.authority];
|
||||
_setResolvedAuthority(resolvedAuthority: ResolvedAuthority, options?: ResolvedOptions): void {
|
||||
if (this._resolveAuthorityRequests.has(resolvedAuthority.authority)) {
|
||||
const request = this._resolveAuthorityRequests.get(resolvedAuthority.authority)!;
|
||||
RemoteAuthorities.set(resolvedAuthority.authority, resolvedAuthority.host, resolvedAuthority.port);
|
||||
request.resolve({ authority: resolvedAuthority, options });
|
||||
this._onDidChangeConnectionData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
setResolvedAuthorityError(authority: string, err: any): void {
|
||||
if (this._resolveAuthorityRequests[authority]) {
|
||||
let request = this._resolveAuthorityRequests[authority];
|
||||
_setResolvedAuthorityError(authority: string, err: any): void {
|
||||
if (this._resolveAuthorityRequests.has(authority)) {
|
||||
const request = this._resolveAuthorityRequests.get(authority)!;
|
||||
request.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
_setAuthorityConnectionToken(authority: string, connectionToken: string): void {
|
||||
this._connectionTokens.set(authority, connectionToken);
|
||||
RemoteAuthorities.setConnectionToken(authority, connectionToken);
|
||||
this._onDidChangeConnectionData.fire();
|
||||
}
|
||||
}
|
||||
|
||||
166
src/vs/platform/remote/node/tunnelService.ts
Normal file
166
src/vs/platform/remote/node/tunnelService.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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 { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
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';
|
||||
|
||||
async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
|
||||
const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
|
||||
return tunnel.waitForReady();
|
||||
}
|
||||
|
||||
class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
|
||||
|
||||
public readonly tunnelRemotePort: number;
|
||||
public tunnelLocalPort!: number;
|
||||
public tunnelRemoteHost: string;
|
||||
public localAddress!: string;
|
||||
|
||||
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, 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 dispose(): 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;
|
||||
address = (<net.AddressInfo>this._server.listen(localPort).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;
|
||||
address = (<net.AddressInfo>this._server.listen(localPort).address());
|
||||
}
|
||||
|
||||
this.tunnelLocalPort = address.port;
|
||||
|
||||
await this._barrier.wait();
|
||||
this.localAddress = '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 protocol = await connectRemoteAgentTunnel(this._options, 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', () => {
|
||||
this._socketsDispose.delete(localSocket.localAddress);
|
||||
remoteSocket.end();
|
||||
});
|
||||
|
||||
localSocket.on('close', () => remoteSocket.end());
|
||||
remoteSocket.on('end', () => localSocket.end());
|
||||
remoteSocket.on('close', () => localSocket.end());
|
||||
|
||||
localSocket.pipe(remoteSocket);
|
||||
remoteSocket.pipe(localSocket);
|
||||
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 TunnelService extends AbstractTunnelService {
|
||||
public constructor(
|
||||
@ILogService logService: ILogService,
|
||||
@ISignService private readonly signService: ISignService,
|
||||
@IProductService private readonly productService: IProductService
|
||||
) {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
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) {
|
||||
++existing.refcount;
|
||||
return existing.value;
|
||||
}
|
||||
|
||||
if (this._tunnelProvider) {
|
||||
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort });
|
||||
if (tunnel) {
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
}
|
||||
return tunnel;
|
||||
} else {
|
||||
const options: IConnectionOptions = {
|
||||
commit: this.productService.commit,
|
||||
socketFactory: nodeSocketFactory,
|
||||
addressProvider: {
|
||||
getAddress: async () => {
|
||||
return resolveRemoteAuthority;
|
||||
}
|
||||
},
|
||||
signService: this.signService,
|
||||
logService: this.logService
|
||||
};
|
||||
|
||||
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
|
||||
this.addTunnelToMap(remoteHost, remotePort, tunnel);
|
||||
return tunnel;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user