@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
279 lines (278 loc) • 10.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import * as net from 'net';
import * as os from 'os';
import { BROWSER_RESTRICTED_PORTS, findFreePortFaster } from '@sussudio/base/node/ports.mjs';
import { nodeSocketFactory } from '../../remote/node/nodeSocketFactory.mjs';
import { Barrier } from '@sussudio/base/common/async.mjs';
import { Disposable } from '@sussudio/base/common/lifecycle.mjs';
import { IConfigurationService } from '../../configuration/common/configuration.mjs';
import { ILogService } from '../../log/common/log.mjs';
import { IProductService } from '../../product/common/productService.mjs';
import { connectRemoteAgentTunnel } from '../../remote/common/remoteAgentConnection.mjs';
import {
AbstractTunnelService,
isAllInterfaces,
isLocalhost,
isPortPrivileged,
TunnelPrivacyId,
} from '../common/tunnel.mjs';
import { ISignService } from '../../sign/common/sign.mjs';
import { OS } from '@sussudio/base/common/platform.mjs';
async function createRemoteTunnel(options, defaultTunnelHost, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort) {
let readyTunnel;
for (let attempts = 3; attempts; attempts--) {
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 {
defaultTunnelHost;
suggestedLocalPort;
tunnelRemotePort;
tunnelLocalPort;
tunnelRemoteHost;
localAddress;
privacy = TunnelPrivacyId.Private;
_options;
_server;
_barrier;
_listeningListener;
_connectionListener;
_errorListener;
_socketsDispose = new Map();
constructor(options, defaultTunnelHost, tunnelRemoteHost, tunnelRemotePort, suggestedLocalPort) {
super();
this.defaultTunnelHost = defaultTunnelHost;
this.suggestedLocalPort = suggestedLocalPort;
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;
}
async dispose() {
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();
});
}
async waitForReady() {
const startPort = this.suggestedLocalPort ?? this.tunnelRemotePort;
const hostname = isAllInterfaces(this.defaultTunnelHost) ? '0.0.0.0' : '127.0.0.1';
// try to get the same port number as the remote port number...
let localPort = await findFreePortFaster(startPort, 2, 1000, hostname);
// if that fails, the method above returns 0, which works out fine below...
let address = null;
this._server.listen(localPort, this.defaultTunnelHost);
await this._barrier.wait();
address = 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 = this._server.address();
}
this.tunnelLocalPort = address.port;
this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`;
return this;
}
async _onConnection(localSocket) {
// 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 = 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();
});
}
}
}
let BaseTunnelService = class BaseTunnelService extends AbstractTunnelService {
socketFactory;
signService;
productService;
constructor(socketFactory, logService, signService, productService, configurationService) {
super(logService, configurationService);
this.socketFactory = socketFactory;
this.signService = signService;
this.productService = productService;
}
isPortPrivileged(port) {
return isPortPrivileged(port, this.defaultTunnelHost, OS, os.release());
}
retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol) {
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 = {
commit: this.productService.commit,
quality: this.productService.quality,
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;
}
}
};
BaseTunnelService = __decorate(
[__param(1, ILogService), __param(2, ISignService), __param(3, IProductService), __param(4, IConfigurationService)],
BaseTunnelService,
);
export { BaseTunnelService };
let TunnelService = class TunnelService extends BaseTunnelService {
constructor(logService, signService, productService, configurationService) {
super(nodeSocketFactory, logService, signService, productService, configurationService);
}
};
TunnelService = __decorate(
[__param(0, ILogService), __param(1, ISignService), __param(2, IProductService), __param(3, IConfigurationService)],
TunnelService,
);
export { TunnelService };
let SharedTunnelsService = class SharedTunnelsService extends Disposable {
logService;
productService;
signService;
configurationService;
_tunnelServices = new Map();
constructor(logService, productService, signService, configurationService) {
super();
this.logService = logService;
this.productService = productService;
this.signService = signService;
this.configurationService = configurationService;
}
async openTunnel(authority, addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, privacy, protocol) {
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);
}
};
SharedTunnelsService = __decorate(
[__param(0, ILogService), __param(1, IProductService), __param(2, ISignService), __param(3, IConfigurationService)],
SharedTunnelsService,
);
export { SharedTunnelsService };