vscode/src/vs/server/remoteExtensionHostAgentServer.ts
2021-10-20 18:42:13 +02:00

1040 lines
41 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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<TContext = string> extends IPCServer<TContext> {
private _onDidConnectEmitter: Emitter<ClientConnectionEvent>;
constructor() {
const emitter = new Emitter<ClientConnectionEvent>();
super(emitter.event);
this._onDidConnectEmitter = emitter;
}
public acceptConnection(protocol: IMessagePassingProtocol, onDidClientDisconnect: Event<void>): 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<RemoteAgentConnectionContext>;
private readonly _uriTransformerCache: { [remoteAuthority: string]: IURITransformer; };
private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection; };
private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection; };
private readonly _allReconnectionTokens: Set<string>;
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<RemoteAgentConnectionContext>();
this._uriTransformerCache = Object.create(null);
this._extHostConnections = Object.create(null);
this._managementConnections = Object.create(null);
this._allReconnectionTokens = new Set<string>();
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<RemoteAgentConnectionContext>(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<number>(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<string, string> = 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<void> {
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 || `<unknown>`;
}
private async _rejectWebSocketConnection(logPrefix: string, protocol: PersistentProtocol, reason: string): Promise<void> {
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 = <typeof vsda>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 = <HandshakeMessage>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 = <HandshakeMessage>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<void> {
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 = <IRemoteExtensionHostStartParams>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 = <ITunnelConnectionStartParams>msg.args;
this._createTunnel(protocol, tunnelStartParams);
} else {
return this._rejectWebSocketConnection(logPrefix, protocol, `Unknown initial data received`);
}
}
private async _createTunnel(protocol: PersistentProtocol, tunnelStartParams: ITunnelConnectionStartParams): Promise<void> {
const remoteSocket = (<NodeSocket>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<net.Socket> {
return new Promise<net.Socket>((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<IRemoteExtensionHostStartParams> {
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<void> {
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<void>;
/**
* 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<IServerAPI> {
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 = (<any>global).vscodeServerStartTime;
const vscodeServerListenTime: number = (<any>global).vscodeServerListenTime;
const vscodeServerCodeLoadedTime: number = (<any>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<ServerStartEvent, ServerStartClassification>('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;
};
})();