/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as crypto from 'crypto'; import * as fs from 'fs'; import * as http from 'http'; import * as net from 'net'; import * as url from 'url'; import { release, hostname } from 'os'; import * as perf from 'vs/base/common/performance'; import { performance } from 'perf_hooks'; import { VSBuffer } from 'vs/base/common/buffer'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises } from 'vs/base/node/pfs'; import { findFreePort } from 'vs/base/node/ports'; import * as platform from 'vs/base/common/platform'; import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { ConnectionType, ConnectionTypeRequest, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, ITunnelConnectionStartParams, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection'; import { ExtensionHostConnection } from 'vs/server/extensionHostConnection'; import { ManagementConnection } from 'vs/server/remoteExtensionManagement'; import { createRemoteURITransformer } from 'vs/server/remoteUriTransformer'; import { ILogService, LogLevel, AbstractLogger, DEFAULT_LOG_LEVEL, MultiplexLogService, getLogLevel, LogService } from 'vs/platform/log/common/log'; import { FileAccess, Schemas } from 'vs/base/common/network'; import product from 'vs/platform/product/common/product'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestService } from 'vs/platform/request/node/requestService'; import { ITelemetryAppender, NullAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; import { ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { getMachineId } from 'vs/base/node/id'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IPCServer, ClientConnectionEvent, IMessagePassingProtocol, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { Emitter, Event } from 'vs/base/common/event'; import { RemoteAgentEnvironmentChannel } from 'vs/server/remoteAgentEnvironmentImpl'; import { RemoteAgentFileSystemChannel } from 'vs/server/remoteAgentFileSystemImpl'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; import { RequestChannel } from 'vs/platform/request/common/requestIpc'; import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import ErrorTelemetry from 'vs/platform/telemetry/node/errorTelemetry'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { LogLevelChannel } from 'vs/platform/log/common/logIpc'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { WebClientServer, serveError, serveFile } from 'vs/server/webClientServer'; import { URI } from 'vs/base/common/uri'; import { isEqualOrParent } from 'vs/base/common/extpath'; import { IServerEnvironmentService, ServerEnvironmentService, ServerParsedArgs } from 'vs/server/serverEnvironmentService'; import { basename, dirname, join } from 'vs/base/common/path'; import { REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { RemoteTerminalChannel } from 'vs/server/remoteTerminalChannel'; import { LoaderStats } from 'vs/base/common/amd'; import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; import { IPtyService, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; import { IRemoteTelemetryService, RemoteNullTelemetryService, RemoteTelemetryService } from 'vs/server/remoteTelemetryService'; const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; const eventPrefix = 'monacoworkbench'; class SocketServer extends IPCServer { private _onDidConnectEmitter: Emitter; constructor() { const emitter = new Emitter(); super(emitter.event); this._onDidConnectEmitter = emitter; } public acceptConnection(protocol: IMessagePassingProtocol, onDidClientDisconnect: Event): void { this._onDidConnectEmitter.fire({ protocol, onDidClientDisconnect }); } } function twodigits(n: number): string { if (n < 10) { return `0${n}`; } return String(n); } function now(): string { const date = new Date(); return `${twodigits(date.getHours())}:${twodigits(date.getMinutes())}:${twodigits(date.getSeconds())}`; } class ServerLogService extends AbstractLogger implements ILogService { _serviceBrand: undefined; private useColors: boolean; constructor(logLevel: LogLevel = DEFAULT_LOG_LEVEL) { super(); this.setLevel(logLevel); this.useColors = Boolean(process.stdout.isTTY); } trace(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Trace) { if (this.useColors) { console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); } else { console.log(`[${now()}]`, message, ...args); } } } debug(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Debug) { if (this.useColors) { console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); } else { console.log(`[${now()}]`, message, ...args); } } } info(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Info) { if (this.useColors) { console.log(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); } else { console.log(`[${now()}]`, message, ...args); } } } warn(message: string | Error, ...args: any[]): void { if (this.getLevel() <= LogLevel.Warning) { if (this.useColors) { console.warn(`\x1b[93m[${now()}]\x1b[0m`, message, ...args); } else { console.warn(`[${now()}]`, message, ...args); } } } error(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Error) { if (this.useColors) { console.error(`\x1b[91m[${now()}]\x1b[0m`, message, ...args); } else { console.error(`[${now()}]`, message, ...args); } } } critical(message: string, ...args: any[]): void { if (this.getLevel() <= LogLevel.Critical) { if (this.useColors) { console.error(`\x1b[90m[${now()}]\x1b[0m`, message, ...args); } else { console.error(`[${now()}]`, message, ...args); } } } override dispose(): void { // noop } flush(): void { // noop } } export type ServerListenOptions = { host?: string; port?: number; socketPath?: string }; declare module vsda { // the signer is a native module that for historical reasons uses a lower case class name // eslint-disable-next-line @typescript-eslint/naming-convention export class signer { sign(arg: string): string; } // eslint-disable-next-line @typescript-eslint/naming-convention export class validator { createNewMessage(arg: string): string; validate(arg: string): 'ok' | 'error'; } } export class RemoteExtensionHostAgentServer extends Disposable { private readonly _logService: ILogService; private readonly _socketServer: SocketServer; private readonly _uriTransformerCache: { [remoteAuthority: string]: IURITransformer; }; private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection; }; private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection; }; private readonly _allReconnectionTokens: Set; private readonly _webClientServer: WebClientServer | null; private shutdownTimer: NodeJS.Timer | undefined; constructor( private readonly _environmentService: IServerEnvironmentService, private readonly _productService: IProductService, private readonly _connectionToken: string, private readonly _connectionTokenIsMandatory: boolean, hasWebClient: boolean, REMOTE_DATA_FOLDER: string ) { super(); const logService = getOrCreateSpdLogService(this._environmentService); logService.trace(`Remote configuration data at ${REMOTE_DATA_FOLDER}`); logService.trace('process arguments:', this._environmentService.args); this._logService = new MultiplexLogService([new ServerLogService(getLogLevel(this._environmentService)), logService]); this._socketServer = new SocketServer(); this._uriTransformerCache = Object.create(null); this._extHostConnections = Object.create(null); this._managementConnections = Object.create(null); this._allReconnectionTokens = new Set(); if (hasWebClient) { this._webClientServer = new WebClientServer(this._connectionToken, this._environmentService, this._logService); } else { this._webClientServer = null; } this._logService.info(`Extension host agent started.`); } public async initialize(): Promise<{ telemetryService: ITelemetryService; }> { const services = await this._createServices(); setTimeout(() => this._cleanupOlderLogs(this._environmentService.logsPath).then(null, err => this._logService.error(err)), 10000); return services; } private async _createServices(): Promise<{ telemetryService: ITelemetryService; }> { const services = new ServiceCollection(); // ExtensionHost Debug broadcast service this._socketServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel()); // TODO: @Sandy @Joao need dynamic context based router const router = new StaticRouter(ctx => ctx.clientId === 'renderer'); this._socketServer.registerChannel('logger', new LogLevelChannel(this._logService)); services.set(IEnvironmentService, this._environmentService); services.set(INativeEnvironmentService, this._environmentService); services.set(ILogService, this._logService); services.set(IProductService, this._productService); // Files const fileService = this._register(new FileService(this._logService)); services.set(IFileService, fileService); fileService.registerProvider(Schemas.file, this._register(new DiskFileSystemProvider(this._logService))); const configurationService = new ConfigurationService(this._environmentService.machineSettingsResource, fileService); services.set(IConfigurationService, configurationService); services.set(IRequestService, new SyncDescriptor(RequestService)); let appInsightsAppender: ITelemetryAppender = NullAppender; if (!this._environmentService.args['disable-telemetry'] && product.enableTelemetry) { if (product.aiConfig && product.aiConfig.asimovKey) { appInsightsAppender = new AppInsightsAppender(eventPrefix, null, product.aiConfig.asimovKey); this._register(toDisposable(() => appInsightsAppender!.flush())); // Ensure the AI appender is disposed so that it flushes remaining data } const machineId = await getMachineId(); const config: ITelemetryServiceConfig = { appenders: [appInsightsAppender], commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, product.commit, product.version + '-remote', machineId, product.msftInternalDomains, this._environmentService.installSourcePath, 'remoteAgent'), piiPaths: [this._environmentService.appRoot] }; services.set(IRemoteTelemetryService, new SyncDescriptor(RemoteTelemetryService, [config])); } else { services.set(IRemoteTelemetryService, RemoteNullTelemetryService); } services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); const downloadChannel = this._socketServer.getChannel('download', router); services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel, () => this._getUriTransformer('renderer') /* TODO: @Sandy @Joao need dynamic context based router */)); services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService = new InstantiationService(services); services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService)); const extensionManagementCLIService = instantiationService.createInstance(ExtensionManagementCLIService); services.set(IExtensionManagementCLIService, extensionManagementCLIService); const ptyService = instantiationService.createInstance( PtyHostService, { GraceTime: ProtocolConstants.ReconnectionGraceTime, ShortGraceTime: ProtocolConstants.ReconnectionShortGraceTime, scrollback: configurationService.getValue(TerminalSettingId.PersistentSessionScrollback) ?? 100 } ); services.set(IPtyService, ptyService); return instantiationService.invokeFunction(accessor => { const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(this._connectionToken, this._environmentService, extensionManagementCLIService, this._logService, accessor.get(IRemoteTelemetryService), appInsightsAppender); this._socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); this._socketServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannel(this._environmentService, this._logService, ptyService)); const remoteFileSystemChannel = new RemoteAgentFileSystemChannel(this._logService, this._environmentService); this._socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); this._socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); const extensionManagementService = accessor.get(IExtensionManagementService); const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => this._getUriTransformer(ctx.remoteAuthority)); this._socketServer.registerChannel('extensions', channel); // clean up deprecated extensions (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); this._register(new ErrorTelemetry(accessor.get(ITelemetryService))); return { telemetryService: accessor.get(ITelemetryService) }; }); } private _getUriTransformer(remoteAuthority: string): IURITransformer { if (!this._uriTransformerCache[remoteAuthority]) { this._uriTransformerCache[remoteAuthority] = createRemoteURITransformer(remoteAuthority); } return this._uriTransformerCache[remoteAuthority]; } public async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { // Only serve GET requests if (req.method !== 'GET') { return serveError(req, res, 405, `Unsupported method ${req.method}`); } if (!req.url) { return serveError(req, res, 400, `Bad request.`); } const parsedUrl = url.parse(req.url, true); const pathname = parsedUrl.pathname; if (!pathname) { return serveError(req, res, 400, `Bad request.`); } // Version if (pathname === '/version') { res.writeHead(200, { 'Content-Type': 'text/plain' }); return res.end(product.commit || ''); } // Delay shutdown if (pathname === '/delay-shutdown') { this._delayShutdown(); res.writeHead(200); return res.end('OK'); } if (pathname === '/vscode-remote-resource') { // Handle HTTP requests for resources rendered in the rich client (images, fonts, etc.) // These resources could be files shipped with extensions or even workspace files. if (parsedUrl.query['tkn'] !== this._connectionToken) { return serveError(req, res, 403, `Forbidden.`); } const desiredPath = parsedUrl.query['path']; if (typeof desiredPath !== 'string') { return serveError(req, res, 400, `Bad request.`); } let filePath: string; try { filePath = URI.from({ scheme: Schemas.file, path: desiredPath }).fsPath; } catch (err) { return serveError(req, res, 400, `Bad request.`); } const responseHeaders: Record = Object.create(null); if (this._environmentService.isBuilt) { if (isEqualOrParent(filePath, this._environmentService.builtinExtensionsPath, !platform.isLinux) || isEqualOrParent(filePath, this._environmentService.extensionsPath, !platform.isLinux) ) { responseHeaders['Cache-Control'] = 'public, max-age=31536000'; } } return serveFile(this._logService, req, res, filePath, responseHeaders); } // workbench web UI if (this._webClientServer) { this._webClientServer.handle(req, res, parsedUrl); return; } res.writeHead(404, { 'Content-Type': 'text/plain' }); return res.end('Not found'); } public handleUpgrade(req: http.IncomingMessage, socket: net.Socket) { let reconnectionToken = generateUuid(); let isReconnection = false; let skipWebSocketFrames = false; if (req.url) { const query = url.parse(req.url, true).query; if (typeof query.reconnectionToken === 'string') { reconnectionToken = query.reconnectionToken; } if (query.reconnection === 'true') { isReconnection = true; } if (query.skipWebSocketFrames === 'true') { skipWebSocketFrames = true; } } if (req.headers['upgrade'] !== 'websocket') { socket.end('HTTP/1.1 400 Bad Request'); return; } // https://tools.ietf.org/html/rfc6455#section-4 const requestNonce = req.headers['sec-websocket-key']; const hash = crypto.createHash('sha1'); hash.update(requestNonce + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); const responseNonce = hash.digest('base64'); const responseHeaders = [ `HTTP/1.1 101 Switching Protocols`, `Upgrade: websocket`, `Connection: Upgrade`, `Sec-WebSocket-Accept: ${responseNonce}` ]; // See https://tools.ietf.org/html/rfc7692#page-12 let permessageDeflate = false; if (!skipWebSocketFrames && !this._environmentService.args['disable-websocket-compression'] && req.headers['sec-websocket-extensions']) { const websocketExtensionOptions = Array.isArray(req.headers['sec-websocket-extensions']) ? req.headers['sec-websocket-extensions'] : [req.headers['sec-websocket-extensions']]; for (const websocketExtensionOption of websocketExtensionOptions) { if (/\b((server_max_window_bits)|(server_no_context_takeover)|(client_no_context_takeover))\b/.test(websocketExtensionOption)) { // sorry, the server does not support zlib parameter tweaks continue; } if (/\b(permessage-deflate)\b/.test(websocketExtensionOption)) { permessageDeflate = true; responseHeaders.push(`Sec-WebSocket-Extensions: permessage-deflate`); break; } if (/\b(x-webkit-deflate-frame)\b/.test(websocketExtensionOption)) { permessageDeflate = true; responseHeaders.push(`Sec-WebSocket-Extensions: x-webkit-deflate-frame`); break; } } } socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); // Never timeout this socket due to inactivity! socket.setTimeout(0); // Finally! if (skipWebSocketFrames) { this._handleWebSocketConnection(new NodeSocket(socket), isReconnection, reconnectionToken); } else { this._handleWebSocketConnection(new WebSocketNodeSocket(new NodeSocket(socket), permessageDeflate, null, true), isReconnection, reconnectionToken); } } public handleServerError(err: Error): void { this._logService.error(`Error occurred in server`); this._logService.error(err); } // Eventually cleanup /** * Cleans up older logs, while keeping the 10 most recent ones. */ private async _cleanupOlderLogs(logsPath: string): Promise { const currentLog = basename(logsPath); const logsRoot = dirname(logsPath); const children = await Promises.readdir(logsRoot); const allSessions = children.filter(name => /^\d{8}T\d{6}$/.test(name)); const oldSessions = allSessions.sort().filter((d) => d !== currentLog); const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9)); await Promise.all(toDelete.map(name => Promises.rm(join(logsRoot, name)))); } private _getRemoteAddress(socket: NodeSocket | WebSocketNodeSocket): string { let _socket: net.Socket; if (socket instanceof NodeSocket) { _socket = socket.socket; } else { _socket = socket.socket.socket; } return _socket.remoteAddress || ``; } private async _rejectWebSocketConnection(logPrefix: string, protocol: PersistentProtocol, reason: string): Promise { const socket = protocol.getSocket(); this._logService.error(`${logPrefix} ${reason}.`); const errMessage: ErrorMessage = { type: 'error', reason: reason }; protocol.sendControl(VSBuffer.fromString(JSON.stringify(errMessage))); protocol.dispose(); await socket.drain(); socket.dispose(); } /** * NOTE: Avoid using await in this method! * The problem is that await introduces a process.nextTick due to the implicit Promise.then * This can lead to some bytes being interpreted and a control message being emitted before the next listener has a chance to be registered. */ private _handleWebSocketConnection(socket: NodeSocket | WebSocketNodeSocket, isReconnection: boolean, reconnectionToken: string): void { const remoteAddress = this._getRemoteAddress(socket); const logPrefix = `[${remoteAddress}][${reconnectionToken.substr(0, 8)}]`; const protocol = new PersistentProtocol(socket); let validator: vsda.validator; let signer: vsda.signer; try { const vsdaMod = require.__$__nodeRequire('vsda'); validator = new vsdaMod.validator(); signer = new vsdaMod.signer(); } catch (e) { } const enum State { WaitingForAuth, WaitingForConnectionType, Done, Error } let state = State.WaitingForAuth; const rejectWebSocketConnection = (msg: string) => { state = State.Error; listener.dispose(); this._rejectWebSocketConnection(logPrefix, protocol, msg); }; const listener = protocol.onControlMessage((raw) => { if (state === State.WaitingForAuth) { let msg1: HandshakeMessage; try { msg1 = JSON.parse(raw.toString()); } catch (err) { return rejectWebSocketConnection(`Malformed first message`); } if (msg1.type !== 'auth') { return rejectWebSocketConnection(`Invalid first message`); } if (this._connectionTokenIsMandatory && msg1.auth !== this._connectionToken) { return rejectWebSocketConnection(`Unauthorized client refused: auth mismatch`); } // Send `sign` request let signedData = generateUuid(); if (signer) { try { signedData = signer.sign(msg1.data); } catch (e) { } } let someText = generateUuid(); if (validator) { try { someText = validator.createNewMessage(someText); } catch (e) { } } const signRequest: SignRequest = { type: 'sign', data: someText, signedData: signedData }; protocol.sendControl(VSBuffer.fromString(JSON.stringify(signRequest))); state = State.WaitingForConnectionType; } else if (state === State.WaitingForConnectionType) { let msg2: HandshakeMessage; try { msg2 = JSON.parse(raw.toString()); } catch (err) { return rejectWebSocketConnection(`Malformed second message`); } if (msg2.type !== 'connectionType') { return rejectWebSocketConnection(`Invalid second message`); } if (typeof msg2.signedData !== 'string') { return rejectWebSocketConnection(`Invalid second message field type`); } const rendererCommit = msg2.commit; const myCommit = product.commit; if (rendererCommit && myCommit) { // Running in the built version where commits are defined if (rendererCommit !== myCommit) { return rejectWebSocketConnection(`Client refused: version mismatch`); } } let valid = false; if (!validator) { valid = true; } else if (msg2.signedData === this._connectionToken) { // web client valid = true; } else { try { valid = validator.validate(msg2.signedData) === 'ok'; } catch (e) { } } if (!valid) { if (this._environmentService.isBuilt) { return rejectWebSocketConnection(`Unauthorized client refused`); } else { this._logService.error(`${logPrefix} Unauthorized client handshake failed but we proceed because of dev mode.`); } } // We have received a new connection. // This indicates that the server owner has connectivity. // Therefore we will shorten the reconnection grace period for disconnected connections! for (let key in this._managementConnections) { const managementConnection = this._managementConnections[key]; managementConnection.shortenReconnectionGraceTimeIfNecessary(); } for (let key in this._extHostConnections) { const extHostConnection = this._extHostConnections[key]; extHostConnection.shortenReconnectionGraceTimeIfNecessary(); } state = State.Done; listener.dispose(); this._handleConnectionType(remoteAddress, logPrefix, protocol, socket, isReconnection, reconnectionToken, msg2); } }); } private async _handleConnectionType(remoteAddress: string, _logPrefix: string, protocol: PersistentProtocol, socket: NodeSocket | WebSocketNodeSocket, isReconnection: boolean, reconnectionToken: string, msg: ConnectionTypeRequest): Promise { const logPrefix = ( msg.desiredConnectionType === ConnectionType.Management ? `${_logPrefix}[ManagementConnection]` : msg.desiredConnectionType === ConnectionType.ExtensionHost ? `${_logPrefix}[ExtensionHostConnection]` : _logPrefix ); if (msg.desiredConnectionType === ConnectionType.Management) { // This should become a management connection if (isReconnection) { // This is a reconnection if (!this._managementConnections[reconnectionToken]) { if (!this._allReconnectionTokens.has(reconnectionToken)) { // This is an unknown reconnection token return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (never seen)`); } else { // This is a connection that was seen in the past, but is no longer valid return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (seen before)`); } } protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' }))); const dataChunk = protocol.readEntireBuffer(); protocol.dispose(); this._managementConnections[reconnectionToken].acceptReconnection(remoteAddress, socket, dataChunk); } else { // This is a fresh connection if (this._managementConnections[reconnectionToken]) { // Cannot have two concurrent connections using the same reconnection token return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`); } protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' }))); const con = new ManagementConnection(this._logService, reconnectionToken, remoteAddress, protocol); this._socketServer.acceptConnection(con.protocol, con.onClose); this._managementConnections[reconnectionToken] = con; this._allReconnectionTokens.add(reconnectionToken); con.onClose(() => { delete this._managementConnections[reconnectionToken]; }); } } else if (msg.desiredConnectionType === ConnectionType.ExtensionHost) { // This should become an extension host connection const startParams0 = msg.args || { language: 'en' }; const startParams = await this._updateWithFreeDebugPort(startParams0); if (startParams.port) { this._logService.trace(`${logPrefix} - startParams debug port ${startParams.port}`); } this._logService.trace(`${logPrefix} - startParams language: ${startParams.language}`); this._logService.trace(`${logPrefix} - startParams env: ${JSON.stringify(startParams.env)}`); if (isReconnection) { // This is a reconnection if (!this._extHostConnections[reconnectionToken]) { if (!this._allReconnectionTokens.has(reconnectionToken)) { // This is an unknown reconnection token return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (never seen)`); } else { // This is a connection that was seen in the past, but is no longer valid return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown reconnection token (seen before)`); } } protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {}))); const dataChunk = protocol.readEntireBuffer(); protocol.dispose(); this._extHostConnections[reconnectionToken].acceptReconnection(remoteAddress, socket, dataChunk); } else { // This is a fresh connection if (this._extHostConnections[reconnectionToken]) { // Cannot have two concurrent connections using the same reconnection token return this._rejectWebSocketConnection(logPrefix, protocol, `Duplicate reconnection token`); } protocol.sendControl(VSBuffer.fromString(JSON.stringify(startParams.port ? { debugPort: startParams.port } : {}))); const dataChunk = protocol.readEntireBuffer(); protocol.dispose(); const con = new ExtensionHostConnection(this._environmentService, this._logService, reconnectionToken, remoteAddress, socket, dataChunk); this._extHostConnections[reconnectionToken] = con; this._allReconnectionTokens.add(reconnectionToken); con.onClose(() => { delete this._extHostConnections[reconnectionToken]; this._onDidCloseExtHostConnection(); }); con.start(startParams); } } else if (msg.desiredConnectionType === ConnectionType.Tunnel) { const tunnelStartParams = msg.args; this._createTunnel(protocol, tunnelStartParams); } else { return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown initial data received`); } } private async _createTunnel(protocol: PersistentProtocol, tunnelStartParams: ITunnelConnectionStartParams): Promise { const remoteSocket = (protocol.getSocket()).socket; const dataChunk = protocol.readEntireBuffer(); protocol.dispose(); remoteSocket.pause(); const localSocket = await this._connectTunnelSocket(tunnelStartParams.host, tunnelStartParams.port); if (dataChunk.byteLength > 0) { localSocket.write(dataChunk.buffer); } localSocket.on('end', () => remoteSocket.end()); localSocket.on('close', () => remoteSocket.end()); localSocket.on('error', () => remoteSocket.destroy()); remoteSocket.on('end', () => localSocket.end()); remoteSocket.on('close', () => localSocket.end()); remoteSocket.on('error', () => localSocket.destroy()); localSocket.pipe(remoteSocket); remoteSocket.pipe(localSocket); } private _connectTunnelSocket(host: string, port: number): Promise { return new Promise((c, e) => { const socket = net.createConnection( { host: host, port: port }, () => { socket.removeListener('error', e); socket.pause(); c(socket); } ); socket.once('error', e); }); } private _updateWithFreeDebugPort(startParams: IRemoteExtensionHostStartParams): Thenable { if (typeof startParams.port === 'number') { return findFreePort(startParams.port, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */).then(freePort => { startParams.port = freePort; return startParams; }); } // No port clear debug configuration. startParams.debugId = undefined; startParams.port = undefined; startParams.break = undefined; return Promise.resolve(startParams); } private async _onDidCloseExtHostConnection(): Promise { if (!this._environmentService.args['enable-remote-auto-shutdown']) { return; } this._cancelShutdown(); const hasActiveExtHosts = !!Object.keys(this._extHostConnections).length; if (!hasActiveExtHosts) { console.log('Last EH closed, waiting before shutting down'); this._logService.info('Last EH closed, waiting before shutting down'); this._waitThenShutdown(); } } private _waitThenShutdown(): void { if (!this._environmentService.args['enable-remote-auto-shutdown']) { return; } if (this._environmentService.args['remote-auto-shutdown-without-delay']) { this._shutdown(); } else { this.shutdownTimer = setTimeout(() => { this.shutdownTimer = undefined; this._shutdown(); }, SHUTDOWN_TIMEOUT); } } private _shutdown(): void { const hasActiveExtHosts = !!Object.keys(this._extHostConnections).length; if (hasActiveExtHosts) { console.log('New EH opened, aborting shutdown'); this._logService.info('New EH opened, aborting shutdown'); return; } else { console.log('Last EH closed, shutting down'); this._logService.info('Last EH closed, shutting down'); this.dispose(); process.exit(0); } } /** * If the server is in a shutdown timeout, cancel it and start over */ private _delayShutdown(): void { if (this.shutdownTimer) { console.log('Got delay-shutdown request while in shutdown timeout, delaying'); this._logService.info('Got delay-shutdown request while in shutdown timeout, delaying'); this._cancelShutdown(); this._waitThenShutdown(); } } private _cancelShutdown(): void { if (this.shutdownTimer) { console.log('Cancelling previous shutdown timeout'); this._logService.info('Cancelling previous shutdown timeout'); clearTimeout(this.shutdownTimer); this.shutdownTimer = undefined; } } } function parseConnectionToken(args: ServerParsedArgs): { connectionToken: string; connectionTokenIsMandatory: boolean; } { if (args['connection-secret']) { if (args['connectionToken']) { console.warn(`Please do not use the argument connectionToken at the same time as connection-secret.`); process.exit(1); } let rawConnectionToken = fs.readFileSync(args['connection-secret']).toString(); rawConnectionToken = rawConnectionToken.replace(/\r?\n$/, ''); if (!/^[0-9A-Za-z\-]+$/.test(rawConnectionToken)) { console.warn(`The secret defined in ${args['connection-secret']} does not adhere to the characters 0-9, a-z, A-Z or -.`); process.exit(1); } return { connectionToken: rawConnectionToken, connectionTokenIsMandatory: true }; } else { return { connectionToken: args['connectionToken'] || generateUuid(), connectionTokenIsMandatory: false }; } } export interface IServerAPI { /** * Do not remove!!. Called from vs/server/main.js */ handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise; /** * Do not remove!!. Called from vs/server/main.js */ handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void; /** * Do not remove!!. Called from vs/server/main.js */ handleServerError(err: Error): void; /** * Do not remove!!. Called from vs/server/main.js */ dispose(): void; } export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise { const productService = { _serviceBrand: undefined, ...product }; const environmentService = new ServerEnvironmentService(args, productService); // // On Windows, exit early with warning message to users about potential security issue // if there is node_modules folder under home drive or Users folder. // if (process.platform === 'win32' && process.env.HOMEDRIVE && process.env.HOMEPATH) { const homeDirModulesPath = join(process.env.HOMEDRIVE, 'node_modules'); const userDir = dirname(join(process.env.HOMEDRIVE, process.env.HOMEPATH)); const userDirModulesPath = join(userDir, 'node_modules'); if (fs.existsSync(homeDirModulesPath) || fs.existsSync(userDirModulesPath)) { const message = ` * * !!!! Server terminated due to presence of CVE-2020-1416 !!!! * * Please remove the following directories and re-try * ${homeDirModulesPath} * ${userDirModulesPath} * * For more information on the vulnerability https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1416 * `; const logService = getOrCreateSpdLogService(environmentService); logService.warn(message); console.warn(message); process.exit(0); } } const { connectionToken, connectionTokenIsMandatory } = parseConnectionToken(args); const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html', require).fsPath); if (hasWebClient && address && typeof address !== 'string') { // ships the web ui! console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/?tkn=${connectionToken}`); } const remoteExtensionHostAgentServer = new RemoteExtensionHostAgentServer(environmentService, productService, connectionToken, connectionTokenIsMandatory, hasWebClient, REMOTE_DATA_FOLDER); const services = await remoteExtensionHostAgentServer.initialize(); const { telemetryService } = services; perf.mark('code/server/ready'); const currentTime = performance.now(); const vscodeServerStartTime: number = (global).vscodeServerStartTime; const vscodeServerListenTime: number = (global).vscodeServerListenTime; const vscodeServerCodeLoadedTime: number = (global).vscodeServerCodeLoadedTime; type ServerStartClassification = { startTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; startedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; codeLoadedTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; readyTime: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; }; type ServerStartEvent = { startTime: number; startedTime: number; codeLoadedTime: number; readyTime: number; }; telemetryService.publicLog2('serverStart', { startTime: vscodeServerStartTime, startedTime: vscodeServerListenTime, codeLoadedTime: vscodeServerCodeLoadedTime, readyTime: currentTime }); if (args['print-startup-performance']) { const stats = LoaderStats.get(); let output = ''; output += '\n\n### Load AMD-module\n'; output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.amdLoad); output += '\n\n### Load commonjs-module\n'; output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.nodeRequire); output += '\n\n### Invoke AMD-module factory\n'; output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.amdInvoke); output += '\n\n### Invoke commonjs-module\n'; output += LoaderStats.toMarkdownTable(['Module', 'Duration'], stats.nodeEval); output += `Start-up time: ${vscodeServerListenTime - vscodeServerStartTime}\n`; output += `Code loading time: ${vscodeServerCodeLoadedTime - vscodeServerStartTime}\n`; output += `Initialized time: ${currentTime - vscodeServerStartTime}\n`; output += `\n`; console.log(output); } return remoteExtensionHostAgentServer; } const getOrCreateSpdLogService: (environmentService: IServerEnvironmentService) => ILogService = (function () { let _logService: ILogService | null; return function getLogService(environmentService: IServerEnvironmentService): ILogService { if (!_logService) { _logService = new LogService(new SpdLogLogger(RemoteExtensionLogFileName, join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, getLogLevel(environmentService))); } return _logService; }; })();