tunnelService.ts 6.2 KB
Newer Older
A
Alex Dima 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

A
Alex Dima 已提交
6 7
import * as net from 'net';
import { Barrier } from 'vs/base/common/async';
A
Alex Ross 已提交
8
import { Disposable } from 'vs/base/common/lifecycle';
9
import { findFreePortFaster } from 'vs/base/node/ports';
A
Alex Dima 已提交
10
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
11
import { ILogService } from 'vs/platform/log/common/log';
12
import { IProductService } from 'vs/platform/product/common/productService';
A
Alex Ross 已提交
13
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
14
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
A
Alex Dima 已提交
15
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
I
isidor 已提交
16
import { ISignService } from 'vs/platform/sign/common/sign';
A
Alex Dima 已提交
17

A
Alex Ross 已提交
18
async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
19
	const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort);
A
Alex Dima 已提交
20 21 22 23 24 25
	return tunnel.waitForReady();
}

class NodeRemoteTunnel extends Disposable implements RemoteTunnel {

	public readonly tunnelRemotePort: number;
26
	public tunnelLocalPort!: number;
27
	public tunnelRemoteHost: string;
A
Alex Ross 已提交
28
	public localAddress!: string;
A
Alex Dima 已提交
29 30 31 32 33 34 35

	private readonly _options: IConnectionOptions;
	private readonly _server: net.Server;
	private readonly _barrier: Barrier;

	private readonly _listeningListener: () => void;
	private readonly _connectionListener: (socket: net.Socket) => void;
36
	private readonly _errorListener: () => void;
A
Alex Dima 已提交
37

38 39
	private readonly _socketsDispose: Map<string, () => void> = new Map();

40
	constructor(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
A
Alex Dima 已提交
41 42 43 44 45 46 47 48 49 50 51
		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);

52 53 54 55
		// 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);

A
Alex Dima 已提交
56
		this.tunnelRemotePort = tunnelRemotePort;
57
		this.tunnelRemoteHost = tunnelRemoteHost;
A
Alex Dima 已提交
58 59 60 61 62 63
	}

	public dispose(): void {
		super.dispose();
		this._server.removeListener('listening', this._listeningListener);
		this._server.removeListener('connection', this._connectionListener);
64
		this._server.removeListener('error', this._errorListener);
A
Alex Dima 已提交
65
		this._server.close();
66 67 68 69
		const disposers = Array.from(this._socketsDispose.values());
		disposers.forEach(disposer => {
			disposer();
		});
A
Alex Dima 已提交
70 71 72
	}

	public async waitForReady(): Promise<this> {
73
		// try to get the same port number as the remote port number...
74
		let localPort = await findFreePortFaster(this.suggestedLocalPort ?? this.tunnelRemotePort, 2, 1000);
75 76

		// if that fails, the method above returns 0, which works out fine below...
77 78 79 80 81 82 83 84 85
		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());
		}

A
Alex Ross 已提交
86
		this.tunnelLocalPort = address.port;
87

A
Alex Dima 已提交
88
		await this._barrier.wait();
89
		this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
A
Alex Dima 已提交
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
		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);
		}

106 107 108 109 110
		localSocket.on('end', () => {
			this._socketsDispose.delete(localSocket.localAddress);
			remoteSocket.end();
		});

A
Alex Dima 已提交
111 112 113 114 115 116
		localSocket.on('close', () => remoteSocket.end());
		remoteSocket.on('end', () => localSocket.end());
		remoteSocket.on('close', () => localSocket.end());

		localSocket.pipe(remoteSocket);
		remoteSocket.pipe(localSocket);
117 118 119 120 121
		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();
		});
A
Alex Dima 已提交
122 123
	}
}
A
Alex Dima 已提交
124

A
Alex Ross 已提交
125
export class TunnelService extends AbstractTunnelService {
A
Alex Dima 已提交
126
	public constructor(
A
Alex Ross 已提交
127
		@ILogService logService: ILogService,
128
		@ISignService private readonly signService: ISignService,
129
		@IProductService private readonly productService: IProductService
A
Alex Ross 已提交
130
	) {
131
		super(logService);
132 133
	}

A
Alex Ross 已提交
134
	protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
135
		const existing = this.getTunnelFromMap(remoteHost, remotePort);
136 137 138 139 140
		if (existing) {
			++existing.refcount;
			return existing.value;
		}

141
		if (this._tunnelProvider) {
142
			const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort });
143
			if (tunnel) {
144
				this.addTunnelToMap(remoteHost, remotePort, tunnel);
145 146
			}
			return tunnel;
147
		} else {
148
			const options: IConnectionOptions = {
149
				commit: this.productService.commit,
150
				socketFactory: nodeSocketFactory,
A
Alex Ross 已提交
151
				addressProvider,
152
				signService: this.signService,
A
Alex Dima 已提交
153 154
				logService: this.logService,
				ipcLogger: null
155 156
			};

157
			const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
158
			this.addTunnelToMap(remoteHost, remotePort, tunnel);
159 160
			return tunnel;
		}
A
Alex Dima 已提交
161 162
	}
}