UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

279 lines (278 loc) 10.5 kB
/*--------------------------------------------------------------------------------------------- * 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 };