server.ts 35.1 KB
Newer Older
A
Asher 已提交
1
import * as crypto from "crypto";
A
Asher 已提交
2 3
import * as fs from "fs";
import * as http from "http";
A
Asher 已提交
4
import * as https from "https";
A
Asher 已提交
5 6
import * as net from "net";
import * as path from "path";
A
Asher 已提交
7
import * as querystring from "querystring";
A
Asher 已提交
8
import { Readable } from "stream";
A
Asher 已提交
9
import * as tls from "tls";
A
Asher 已提交
10
import * as url from "url";
A
Asher 已提交
11
import * as util from "util";
A
Asher 已提交
12
import { Emitter } from "vs/base/common/event";
A
Asher 已提交
13
import { sanitizeFilePath } from "vs/base/common/extpath";
A
Asher 已提交
14 15
import { Schemas } from "vs/base/common/network";
import { URI, UriComponents } from "vs/base/common/uri";
A
Asher 已提交
16
import { generateUuid } from "vs/base/common/uuid";
A
Asher 已提交
17
import { getMachineId } from 'vs/base/node/id';
A
Asher 已提交
18
import { NLSConfiguration } from "vs/base/node/languagePacks";
A
Asher 已提交
19
import { mkdirp, rimraf } from "vs/base/node/pfs";
A
Asher 已提交
20 21
import { ClientConnectionEvent, IPCServer } from "vs/base/parts/ipc/common/ipc";
import { createChannelReceiver } from "vs/base/parts/ipc/node/ipc";
A
Asher 已提交
22
import { LogsDataCleaner } from "vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner";
A
Asher 已提交
23 24
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
import { ConfigurationService } from "vs/platform/configuration/node/configurationService";
A
Asher 已提交
25
import { ExtensionHostDebugBroadcastChannel } from "vs/platform/debug/common/extensionHostDebugIpc";
A
Asher 已提交
26
import { IEnvironmentService, ParsedArgs } from "vs/platform/environment/common/environment";
A
Asher 已提交
27
import { EnvironmentService } from "vs/platform/environment/node/environmentService";
A
Asher 已提交
28 29 30
import { ExtensionGalleryService } from "vs/platform/extensionManagement/common/extensionGalleryService";
import { IExtensionGalleryService, IExtensionManagementService } from "vs/platform/extensionManagement/common/extensionManagement";
import { ExtensionManagementChannel } from "vs/platform/extensionManagement/common/extensionManagementIpc";
A
Asher 已提交
31
import { ExtensionManagementService } from "vs/platform/extensionManagement/node/extensionManagementService";
A
Asher 已提交
32 33 34
import { IFileService } from "vs/platform/files/common/files";
import { FileService } from "vs/platform/files/common/fileService";
import { DiskFileSystemProvider } from "vs/platform/files/node/diskFileSystemProvider";
A
Asher 已提交
35
import { SyncDescriptor } from "vs/platform/instantiation/common/descriptors";
A
Asher 已提交
36
import { InstantiationService } from "vs/platform/instantiation/common/instantiationService";
A
Asher 已提交
37
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
A
Asher 已提交
38 39
import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
import { LocalizationsService } from "vs/platform/localizations/node/localizations";
40
import { getLogLevel, ILogService } from "vs/platform/log/common/log";
A
Asher 已提交
41
import { LoggerChannel } from "vs/platform/log/common/logIpc";
A
Asher 已提交
42
import { SpdLogService } from "vs/platform/log/node/spdlogService";
A
Asher 已提交
43 44
import product from 'vs/platform/product/common/product';
import { IProductService } from "vs/platform/product/common/productService";
45
import { ConnectionType, ConnectionTypeRequest } from "vs/platform/remote/common/remoteAgentConnection";
A
Asher 已提交
46
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel";
A
Asher 已提交
47 48
import { IRequestService } from "vs/platform/request/common/request";
import { RequestChannel } from "vs/platform/request/common/requestIpc";
A
Asher 已提交
49
import { RequestService } from "vs/platform/request/node/requestService";
A
Asher 已提交
50
import ErrorTelemetry from "vs/platform/telemetry/browser/errorTelemetry";
A
Asher 已提交
51
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
A
Asher 已提交
52 53
import { ITelemetryServiceConfig, TelemetryService } from "vs/platform/telemetry/common/telemetryService";
import { combinedAppender, LogAppender, NullTelemetryService } from "vs/platform/telemetry/common/telemetryUtils";
A
Asher 已提交
54 55
import { AppInsightsAppender } from "vs/platform/telemetry/node/appInsightsAppender";
import { resolveCommonProperties } from "vs/platform/telemetry/node/commonProperties";
A
Asher 已提交
56 57 58
import { UpdateChannel } from "vs/platform/update/electron-main/updateIpc";
import { INodeProxyService, NodeProxyChannel } from "vs/server/src/common/nodeProxy";
import { TelemetryChannel } from "vs/server/src/common/telemetry";
A
Asher 已提交
59
import { split } from "vs/server/src/common/util";
60 61 62 63 64 65 66
import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from "vs/server/src/node/channel";
import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/src/node/connection";
import { TelemetryClient } from "vs/server/src/node/insights";
import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls";
import { Protocol } from "vs/server/src/node/protocol";
import { UpdateService } from "vs/server/src/node/update";
import { AuthType, getMediaMime, getUriTransformer, localRequire, tmpdir } from "vs/server/src/node/util";
A
Asher 已提交
67 68
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
A
Asher 已提交
69

A
Asher 已提交
70 71
const tarFs = localRequire<typeof import("tar-fs")>("tar-fs/index");

A
Asher 已提交
72
export enum HttpCode {
A
Asher 已提交
73
	Ok = 200,
A
Asher 已提交
74
	Redirect = 302,
A
Asher 已提交
75 76
	NotFound = 404,
	BadRequest = 400,
A
Asher 已提交
77 78 79
	Unauthorized = 401,
	LargePayload = 413,
	ServerError = 500,
A
Asher 已提交
80 81
}

A
Asher 已提交
82
export interface Options {
A
Asher 已提交
83
	WORKBENCH_WEB_CONFIGURATION: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents };
A
Asher 已提交
84
	REMOTE_USER_DATA_URI: UriComponents | URI;
A
Asher 已提交
85
	PRODUCT_CONFIGURATION: Partial<IProductService>;
A
Asher 已提交
86
	NLS_CONFIGURATION: NLSConfiguration;
A
Asher 已提交
87 88
}

89
export interface Response {
A
Asher 已提交
90
	cache?: boolean;
91
	code?: number;
A
Asher 已提交
92 93 94
	content?: string | Buffer;
	filePath?: string;
	headers?: http.OutgoingHttpHeaders;
A
Asher 已提交
95
	mime?: string;
A
Asher 已提交
96
	redirect?: string;
A
Asher 已提交
97
	stream?: Readable;
A
Asher 已提交
98 99 100
}

export interface LoginPayload {
A
Asher 已提交
101
	password?: string[] | string;
102 103
}

A
Asher 已提交
104
export class HttpError extends Error {
A
Asher 已提交
105 106 107 108 109 110 111 112
	public constructor(message: string, public readonly code: number) {
		super(message);
		// @ts-ignore
		this.name = this.constructor.name;
		Error.captureStackTrace(this, this.constructor);
	}
}

A
Asher 已提交
113
export interface ServerOptions {
A
Asher 已提交
114
	readonly auth: AuthType;
A
Asher 已提交
115
	readonly basePath?: string;
A
Asher 已提交
116
	readonly connectionToken?: string;
A
Asher 已提交
117 118
	readonly cert?: string;
	readonly certKey?: string;
119
	readonly openUri?: string;
A
Asher 已提交
120 121 122 123
	readonly host?: string;
	readonly password?: string;
	readonly port?: number;
	readonly socket?: string;
A
Asher 已提交
124 125
}

A
Asher 已提交
126
export abstract class Server {
A
Asher 已提交
127
	protected readonly server: http.Server | https.Server;
128
	protected rootPath = path.resolve(__dirname, "../../../../..");
A
Asher 已提交
129 130
	protected serverRoot = path.join(this.rootPath, "/out/vs/server/src");
	protected readonly allowedRequestPaths: string[] = [this.rootPath];
A
Asher 已提交
131
	private listenPromise: Promise<string> | undefined;
A
Asher 已提交
132
	public readonly protocol: "http" | "https";
A
Asher 已提交
133 134 135 136
	public readonly options: ServerOptions;

	public constructor(options: ServerOptions) {
		this.options = {
A
Asher 已提交
137
			host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
A
Asher 已提交
138
			...options,
A
Asher 已提交
139
			basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
A
Asher 已提交
140 141 142
		};
		this.protocol = this.options.cert ? "https" : "http";
		if (this.protocol === "https") {
143
			const httpolyglot = localRequire<typeof import("httpolyglot")>("httpolyglot/lib/index");
A
Asher 已提交
144
			this.server = httpolyglot.createServer({
A
Asher 已提交
145 146
				cert: this.options.cert && fs.readFileSync(this.options.cert),
				key: this.options.certKey && fs.readFileSync(this.options.certKey),
A
Asher 已提交
147 148 149 150
			}, this.onRequest);
		} else {
			this.server = http.createServer(this.onRequest);
		}
A
Asher 已提交
151 152
	}

A
Asher 已提交
153 154 155 156
	public listen(): Promise<string> {
		if (!this.listenPromise) {
			this.listenPromise = new Promise((resolve, reject) => {
				this.server.on("error", reject);
A
Asher 已提交
157
				this.server.on("upgrade", this.onUpgrade);
A
Asher 已提交
158
				const onListen = () => resolve(this.address());
A
Asher 已提交
159 160 161 162 163
				if (this.options.socket) {
					this.server.listen(this.options.socket, onListen);
				} else {
					this.server.listen(this.options.port, this.options.host, onListen);
				}
A
Asher 已提交
164 165 166
			});
		}
		return this.listenPromise;
A
Asher 已提交
167 168
	}

A
Asher 已提交
169
	/**
A
Asher 已提交
170
	 * The *local* address of the server.
A
Asher 已提交
171
	 */
A
Asher 已提交
172
	public address(): string {
A
Asher 已提交
173 174
		const address = this.server.address();
		const endpoint = typeof address !== "string"
A
Asher 已提交
175
			? (address.address === "::" ? "localhost" : address.address) + ":" + address.port
A
Asher 已提交
176
			: address;
A
Asher 已提交
177
		return `${this.protocol}://${endpoint}`;
A
Asher 已提交
178 179
	}

A
Asher 已提交
180 181 182 183 184
	protected abstract handleWebSocket(
		socket: net.Socket,
		parsedUrl: url.UrlWithParsedQuery
	): Promise<void>;

A
Asher 已提交
185 186 187 188 189 190 191
	protected abstract handleRequest(
		base: string,
		requestPath: string,
		parsedUrl: url.UrlWithParsedQuery,
		request: http.IncomingMessage,
	): Promise<Response>;

A
Asher 已提交
192
	protected async getResource(...parts: string[]): Promise<Response> {
A
Asher 已提交
193 194 195 196
		const filePath = this.ensureAuthorizedFilePath(...parts);
		return { content: await util.promisify(fs.readFile)(filePath), filePath };
	}

197 198 199 200 201
	protected async getAnyResource(...parts: string[]): Promise<Response> {
		const filePath = path.join(...parts);
		return { content: await util.promisify(fs.readFile)(filePath), filePath };
	}

A
Asher 已提交
202 203
	protected async getTarredResource(...parts: string[]): Promise<Response> {
		const filePath = this.ensureAuthorizedFilePath(...parts);
A
Asher 已提交
204
		return { stream: tarFs.pack(filePath), filePath, mime: "application/tar", cache: true };
A
Asher 已提交
205 206 207
	}

	protected ensureAuthorizedFilePath(...parts: string[]): string {
A
Asher 已提交
208
		const filePath = path.join(...parts);
A
Asher 已提交
209 210 211
		if (!this.isAllowedRequestPath(filePath)) {
			throw new HttpError("Unauthorized", HttpCode.Unauthorized);
		}
A
Asher 已提交
212
		return filePath;
A
Asher 已提交
213
	}
A
Asher 已提交
214

A
Asher 已提交
215
	protected withBase(request: http.IncomingMessage, path: string): string {
A
Asher 已提交
216 217
		const [, query] = request.url ? split(request.url, "?") : [];
		return `${this.protocol}://${request.headers.host}${this.options.basePath}${path}${query ? `?${query}` : ""}`;
A
Asher 已提交
218 219
	}

A
Asher 已提交
220 221 222 223 224 225 226 227 228
	private isAllowedRequestPath(path: string): boolean {
		for (let i = 0; i < this.allowedRequestPaths.length; ++i) {
			if (path.indexOf(this.allowedRequestPaths[i]) === 0) {
				return true;
			}
		}
		return false;
	}

A
Asher 已提交
229
	private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
A
Asher 已提交
230
		try {
A
Asher 已提交
231 232
			const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}};
			const payload = await this.preHandleRequest(request, parsedUrl);
A
Asher 已提交
233
			response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
A
Asher 已提交
234
				"Content-Type": payload.mime || getMediaMime(payload.filePath),
A
Asher 已提交
235
				...(payload.redirect ? { Location: this.withBase(request, payload.redirect) } : {}),
236
				...(request.headers["service-worker"] ? { "Service-Worker-Allowed": this.options.basePath || "/" } : {}),
A
Asher 已提交
237
				...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}),
A
Asher 已提交
238 239
				...payload.headers,
			});
A
Asher 已提交
240 241 242 243 244 245 246 247 248
			if (payload.stream) {
				payload.stream.on("error", (error: NodeJS.ErrnoException) => {
					response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError);
					response.end(error.message);
				});
				payload.stream.pipe(response);
			} else {
				response.end(payload.content);
			}
A
Asher 已提交
249 250 251 252 253 254 255 256 257
		} catch (error) {
			if (error.code === "ENOENT" || error.code === "EISDIR") {
				error = new HttpError("Not found", HttpCode.NotFound);
			}
			response.writeHead(typeof error.code === "number" ? error.code : HttpCode.ServerError);
			response.end(error.message);
		}
	}

A
Asher 已提交
258
	private async preHandleRequest(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
A
Asher 已提交
259
		const secure = (request.connection as tls.TLSSocket).encrypted;
A
Asher 已提交
260
		if (this.options.cert && !secure) {
A
Asher 已提交
261
			return { redirect: request.url };
A
Asher 已提交
262 263
		}

A
Asher 已提交
264 265
		const fullPath = decodeURIComponent(parsedUrl.pathname || "/");
		const match = fullPath.match(/^(\/?[^/]*)(.*)$/);
266
		let [/* ignore */, base, requestPath] = match
A
Asher 已提交
267
			? match.map((p) => p.replace(/\/+$/, ""))
A
Asher 已提交
268 269 270 271 272 273 274
			: ["", "", ""];
		if (base.indexOf(".") !== -1) { // Assume it's a file at the root.
			requestPath = base;
			base = "/";
		} else if (base === "") { // Happens if it's a plain `domain.com`.
			base = "/";
		}
A
Asher 已提交
275
		base = path.normalize(base);
A
Asher 已提交
276
		requestPath = path.normalize(requestPath || "/index.html");
A
Asher 已提交
277

A
Asher 已提交
278
		if (base !== "/login" || this.options.auth !== "password" || requestPath !== "/index.html") {
A
Asher 已提交
279 280 281
			this.ensureGet(request);
		}

A
Asher 已提交
282 283 284 285 286
		// Allow for a versioned static endpoint. This lets us cache every static
		// resource underneath the path based on the version without any work and
		// without adding query parameters which have their own issues.
		// REVIEW: Discuss whether this is the best option; this is sort of a quick
		// hack almost to get caching in the meantime but it does work pretty well.
A
Asher 已提交
287
		if (/^\/static-/.test(base)) {
A
Asher 已提交
288 289 290
			base = "/static";
		}

A
Asher 已提交
291 292
		switch (base) {
			case "/":
A
Asher 已提交
293 294 295
				switch (requestPath) {
					case "/favicon.ico":
					case "/manifest.json":
A
Asher 已提交
296 297 298
						const response = await this.getResource(this.serverRoot, "media", requestPath);
						response.cache = true;
						return response;
A
Asher 已提交
299 300
				}
				if (!this.authenticate(request)) {
A
Asher 已提交
301
					return { redirect: "/login" };
A
Asher 已提交
302 303
				}
				break;
A
Asher 已提交
304
			case "/static":
A
Asher 已提交
305 306 307
				const response = await this.getResource(this.rootPath, requestPath);
				response.cache = true;
				return response;
A
Asher 已提交
308
			case "/login":
A
Asher 已提交
309
				if (this.options.auth !== "password" || requestPath !== "/index.html") {
A
Asher 已提交
310 311
					throw new HttpError("Not found", HttpCode.NotFound);
				}
A
Asher 已提交
312
				return this.tryLogin(request);
A
Asher 已提交
313 314
			default:
				if (!this.authenticate(request)) {
A
Asher 已提交
315
					throw new HttpError("Unauthorized", HttpCode.Unauthorized);
A
Asher 已提交
316 317 318
				}
				break;
		}
A
Asher 已提交
319

A
Asher 已提交
320 321
		return this.handleRequest(base, requestPath, parsedUrl, request);
	}
A
Asher 已提交
322

A
Asher 已提交
323 324 325 326 327
	private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket): Promise<void> => {
		try {
			await this.preHandleWebSocket(request, socket);
		} catch (error) {
			socket.destroy();
A
Asher 已提交
328
			console.error(error.message);
A
Asher 已提交
329 330 331 332 333 334 335
		}
	}

	private preHandleWebSocket(request: http.IncomingMessage, socket: net.Socket): Promise<void> {
		socket.on("error", () => socket.destroy());
		socket.on("end", () => socket.destroy());

A
Asher 已提交
336
		this.ensureGet(request);
A
Asher 已提交
337 338
		if (!this.authenticate(request)) {
			throw new HttpError("Unauthorized", HttpCode.Unauthorized);
339
		} else if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") {
A
Asher 已提交
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
			throw new Error("HTTP/1.1 400 Bad Request");
		}

		// This magic value is specified by the websocket spec.
		const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
		const reply = crypto.createHash("sha1")
			.update(<string>request.headers["sec-websocket-key"] + magic)
			.digest("base64");
		socket.write([
			"HTTP/1.1 101 Switching Protocols",
			"Upgrade: websocket",
			"Connection: Upgrade",
			`Sec-WebSocket-Accept: ${reply}`,
		].join("\r\n") + "\r\n\r\n");

		const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}};
		return this.handleWebSocket(socket, parsedUrl);
	}

A
Asher 已提交
359
	private async tryLogin(request: http.IncomingMessage): Promise<Response> {
A
Asher 已提交
360 361 362 363 364 365 366 367 368 369 370
		const redirect = (password?: string | string[] | true) => {
			return {
				redirect: "/",
				headers: typeof password === "string"
					? { "Set-Cookie": `password=${password}; Path=${this.options.basePath || "/"}; HttpOnly; SameSite=strict` }
					: {},
			};
		};
		const providedPassword = this.authenticate(request);
		if (providedPassword && (request.method === "GET" || request.method === "POST")) {
			return redirect(providedPassword);
A
Asher 已提交
371 372 373 374
		}
		if (request.method === "POST") {
			const data = await this.getData<LoginPayload>(request);
			if (this.authenticate(request, data)) {
A
Asher 已提交
375
				return redirect(data.password);
A
Asher 已提交
376
			}
A
Asher 已提交
377 378 379
			console.error("Failed login attempt", JSON.stringify({
				xForwardedFor: request.headers["x-forwarded-for"],
				remoteAddress: request.connection.remoteAddress,
A
Asher 已提交
380 381
				userAgent: request.headers["user-agent"],
				timestamp: Math.floor(new Date().getTime() / 1000),
A
Asher 已提交
382 383
			}));
			return this.getLogin("Invalid password", data);
A
Asher 已提交
384
		}
A
Asher 已提交
385 386
		this.ensureGet(request);
		return this.getLogin();
A
Asher 已提交
387 388
	}

A
Asher 已提交
389
	private async getLogin(error: string = "", payload?: LoginPayload): Promise<Response> {
A
Asher 已提交
390
		const filePath = path.join(this.serverRoot, "browser/login.html");
A
Asher 已提交
391 392 393 394
		const content = (await util.promisify(fs.readFile)(filePath, "utf8"))
			.replace("{{ERROR}}", error)
			.replace("display:none", error ? "display:block" : "display:none")
			.replace('value=""', `value="${payload && payload.password || ""}"`);
A
Asher 已提交
395 396 397 398 399
		return { content, filePath };
	}

	private ensureGet(request: http.IncomingMessage): void {
		if (request.method !== "GET") {
A
Asher 已提交
400
			throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest);
A
Asher 已提交
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
		}
	}

	private getData<T extends object>(request: http.IncomingMessage): Promise<T> {
		return request.method === "POST"
			? new Promise<T>((resolve, reject) => {
				let body = "";
				const onEnd = (): void => {
					off();
					resolve(querystring.parse(body) as T);
				};
				const onError = (error: Error): void => {
					off();
					reject(error);
				};
				const onData = (d: Buffer): void => {
					body += d;
					if (body.length > 1e6) {
A
Asher 已提交
419
						onError(new HttpError("Payload is too large", HttpCode.LargePayload));
A
Asher 已提交
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
						request.connection.destroy();
					}
				};
				const off = (): void => {
					request.off("error", onError);
					request.off("data", onError);
					request.off("end", onEnd);
				};
				request.on("error", onError);
				request.on("data", onData);
				request.on("end", onEnd);
			})
			: Promise.resolve({} as T);
	}

A
Asher 已提交
435
	private authenticate(request: http.IncomingMessage, payload?: LoginPayload): string | boolean {
A
Asher 已提交
436
		if (this.options.auth !== "password") {
A
Asher 已提交
437 438
			return true;
		}
439
		const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
A
Asher 已提交
440 441 442
		if (typeof payload === "undefined") {
			payload = this.parseCookies<LoginPayload>(request);
		}
A
Asher 已提交
443 444 445 446 447 448 449 450 451
		if (this.options.password && payload.password) {
			const toTest = Array.isArray(payload.password) ? payload.password : [payload.password];
			for (let i = 0; i < toTest.length; ++i) {
				if (safeCompare(toTest[i], this.options.password)) {
					return toTest[i];
				}
			}
		}
		return false;
A
Asher 已提交
452 453 454
	}

	private parseCookies<T extends object>(request: http.IncomingMessage): T {
A
Asher 已提交
455
		const cookies: { [key: string]: string[] } = {};
A
Asher 已提交
456 457
		if (request.headers.cookie) {
			request.headers.cookie.split(";").forEach((keyValue) => {
A
Asher 已提交
458
				const [key, value] = split(keyValue, "=");
A
Asher 已提交
459 460 461 462
				if (!cookies[key]) {
					cookies[key] = [];
				}
				cookies[key].push(decodeURI(value));
A
Asher 已提交
463 464 465
			});
		}
		return cookies as T;
A
Asher 已提交
466
	}
A
Asher 已提交
467 468
}

A
Asher 已提交
469 470 471 472 473 474 475 476 477
interface StartPath {
	path?: string[] | string;
	workspace?: boolean;
}

interface Settings {
	lastVisited?: StartPath;
}

A
Asher 已提交
478 479 480 481 482
export class MainServer extends Server {
	public readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
	public readonly onDidClientConnect = this._onDidClientConnect.event;
	private readonly ipc = new IPCServer(this.onDidClientConnect);

483
	private readonly maxExtraOfflineConnections = 0;
A
Asher 已提交
484 485 486
	private readonly connections = new Map<ConnectionType, Map<string, Connection>>();

	private readonly services = new ServiceCollection();
A
Asher 已提交
487
	private readonly servicesPromise: Promise<void>;
A
Asher 已提交
488

A
Asher 已提交
489 490 491 492 493
	public readonly _onProxyConnect = new Emitter<net.Socket>();
	private proxyPipe = path.join(tmpdir, "tls-proxy");
	private _proxyServer?: Promise<net.Server>;
	private readonly proxyTimeout = 5000;

A
Asher 已提交
494
	private settings: Settings = {};
A
Asher 已提交
495 496 497
	private heartbeatTimer?: NodeJS.Timeout;
	private heartbeatInterval = 60000;
	private lastHeartbeat = 0;
A
Asher 已提交
498

A
Asher 已提交
499
	public constructor(options: ServerOptions, args: ParsedArgs) {
A
Asher 已提交
500
		super(options);
A
Asher 已提交
501
		this.servicesPromise = this.initializeServices(args);
A
Asher 已提交
502
	}
A
Asher 已提交
503

A
Asher 已提交
504 505
	public async listen(): Promise<string> {
		const environment = (this.services.get(IEnvironmentService) as EnvironmentService);
A
Asher 已提交
506 507 508 509
		const [address] = await Promise.all<string>([
			super.listen(), ...[
				environment.extensionsPath,
			].map((p) => mkdirp(p).then(() => p)),
A
Asher 已提交
510 511 512 513
		]);
		return address;
	}

A
Asher 已提交
514
	protected async handleWebSocket(socket: net.Socket, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
A
Asher 已提交
515
		this.heartbeat();
A
Asher 已提交
516 517 518
		if (!parsedUrl.query.reconnectionToken) {
			throw new Error("Reconnection token is missing from query parameters");
		}
A
Asher 已提交
519
		const protocol = new Protocol(await this.createProxy(socket), {
A
Asher 已提交
520
			reconnectionToken: <string>parsedUrl.query.reconnectionToken,
A
Asher 已提交
521 522 523 524 525 526 527 528 529 530 531 532
			reconnection: parsedUrl.query.reconnection === "true",
			skipWebSocketFrames: parsedUrl.query.skipWebSocketFrames === "true",
		});
		try {
			await this.connect(await protocol.handshake(), protocol);
		} catch (error) {
			protocol.sendMessage({ type: "error", reason: error.message });
			protocol.dispose();
			protocol.getSocket().dispose();
		}
	}

A
Asher 已提交
533
	protected async handleRequest(
534
		base: string,
A
Asher 已提交
535
		requestPath: string,
A
Asher 已提交
536 537
		parsedUrl: url.UrlWithParsedQuery,
		request: http.IncomingMessage,
538
	): Promise<Response> {
A
Asher 已提交
539
		this.heartbeat();
540
		switch (base) {
A
Asher 已提交
541
			case "/": return this.getRoot(request, parsedUrl);
A
Asher 已提交
542 543 544
			case "/resource":
			case "/vscode-remote-resource":
				if (typeof parsedUrl.query.path === "string") {
A
Asher 已提交
545
					return this.getAnyResource(parsedUrl.query.path);
546
				}
A
Asher 已提交
547
				break;
A
Asher 已提交
548 549 550 551 552
			case "/tar":
				if (typeof parsedUrl.query.path === "string") {
					return this.getTarredResource(parsedUrl.query.path);
				}
				break;
A
Asher 已提交
553
			case "/webview":
A
Asher 已提交
554
				if (/^\/vscode-resource/.test(requestPath)) {
555
					return this.getAnyResource(requestPath.replace(/^\/vscode-resource(\/file)?/, ""));
556
				}
A
Asher 已提交
557 558 559 560 561
				return this.getResource(
					this.rootPath,
					"out/vs/workbench/contrib/webview/browser/pre",
					requestPath
				);
562
		}
A
Asher 已提交
563
		throw new HttpError("Not found", HttpCode.NotFound);
564
	}
A
Asher 已提交
565

566
	private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
A
Asher 已提交
567
		const filePath = path.join(this.serverRoot, "browser/workbench.html");
A
Asher 已提交
568
		let [content, startPath] = await Promise.all([
A
Asher 已提交
569
			util.promisify(fs.readFile)(filePath, "utf8"),
A
Asher 已提交
570 571
			this.getFirstValidPath([
				{ path: parsedUrl.query.workspace, workspace: true },
572
				{ path: parsedUrl.query.folder, workspace: false },
A
Asher 已提交
573
				(await this.readSettings()).lastVisited,
574
				{ path: this.options.openUri }
A
Asher 已提交
575
			]),
A
Asher 已提交
576 577
			this.servicesPromise,
		]);
A
Asher 已提交
578

A
Asher 已提交
579 580 581 582 583 584 585 586 587
		if (startPath) {
			this.writeSettings({
				lastVisited: {
					path: startPath.uri.fsPath,
					workspace: startPath.workspace
				},
			});
		}

588 589
		const logger = this.services.get(ILogService) as ILogService;
		logger.info("request.url", `"${request.url}"`);
A
Asher 已提交
590

A
Asher 已提交
591 592
		const remoteAuthority = request.headers.host as string;
		const transformer = getUriTransformer(remoteAuthority);
A
Asher 已提交
593 594

		const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
595
		const options: Options = {
A
Asher 已提交
596
			WORKBENCH_WEB_CONFIGURATION: {
A
Asher 已提交
597 598
				workspaceUri: startPath && startPath.workspace ? transformer.transformOutgoing(startPath.uri) : undefined,
				folderUri: startPath && !startPath.workspace ? transformer.transformOutgoing(startPath.uri) : undefined,
599
				remoteAuthority,
A
Asher 已提交
600 601 602 603 604
				logLevel: getLogLevel(environment),
			},
			REMOTE_USER_DATA_URI: transformer.transformOutgoing(URI.file(environment.userDataPath)),
			PRODUCT_CONFIGURATION: {
				extensionsGallery: product.extensionsGallery,
605
			},
A
Asher 已提交
606
			NLS_CONFIGURATION: await getNlsConfiguration(environment.args.locale || await getLocaleFromConfig(environment.userDataPath), environment.userDataPath),
607 608
		};

A
Asher 已提交
609
		content = content.replace(/{{COMMIT}}/g, product.commit || "");
A
Asher 已提交
610 611 612
		for (const key in options) {
			content = content.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key as keyof Options])}'`);
		}
613

A
Asher 已提交
614
		return { content, filePath };
A
Asher 已提交
615 616
	}

A
Asher 已提交
617
	/**
618 619 620
	 * Choose the first valid path. If `workspace` is undefined then either a
	 * workspace or a directory are acceptable. Otherwise it must be a file if a
	 * workspace or a directory otherwise.
A
Asher 已提交
621 622 623 624 625 626 627 628 629 630 631 632 633 634
	 */
	private async getFirstValidPath(startPaths: Array<StartPath | undefined>): Promise<{ uri: URI, workspace?: boolean} | undefined> {
		const logger = this.services.get(ILogService) as ILogService;
		const cwd = process.env.VSCODE_CWD || process.cwd();
		for (let i = 0; i < startPaths.length; ++i) {
			const startPath = startPaths[i];
			if (!startPath) {
				continue;
			}
			const paths = typeof startPath.path === "string" ? [startPath.path] : (startPath.path || []);
			for (let j = 0; j < paths.length; ++j) {
				const uri = URI.file(sanitizeFilePath(paths[j], cwd));
				try {
					const stat = await util.promisify(fs.stat)(uri.fsPath);
635 636
					if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
						return { uri, workspace: !stat.isDirectory() };
A
Asher 已提交
637 638 639 640 641 642 643 644 645
					}
				} catch (error) {
					logger.warn(error.message);
				}
			}
		}
		return undefined;
	}

646
	private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise<void> {
A
Asher 已提交
647 648 649 650
		if (product.commit && message.commit !== product.commit) {
			throw new Error(`Version mismatch (${message.commit} instead of ${product.commit})`);
		}

651 652 653 654 655 656 657 658
		switch (message.desiredConnectionType) {
			case ConnectionType.ExtensionHost:
			case ConnectionType.Management:
				if (!this.connections.has(message.desiredConnectionType)) {
					this.connections.set(message.desiredConnectionType, new Map());
				}
				const connections = this.connections.get(message.desiredConnectionType)!;

A
Asher 已提交
659 660 661 662 663 664 665
				const ok = async () => {
					return message.desiredConnectionType === ConnectionType.ExtensionHost
						? { debugPort: await this.getDebugPort() }
						: { type: "ok" };
				};

				const token = protocol.options.reconnectionToken;
666
				if (protocol.options.reconnection && connections.has(token)) {
A
Asher 已提交
667
					protocol.sendMessage(await ok());
668 669
					const buffer = protocol.readEntireBuffer();
					protocol.dispose();
A
Asher 已提交
670
					return connections.get(token)!.reconnect(protocol.getSocket(), buffer);
A
Asher 已提交
671
				} else if (protocol.options.reconnection || connections.has(token)) {
672 673 674 675 676 677
					throw new Error(protocol.options.reconnection
						? "Unrecognized reconnection token"
						: "Duplicate reconnection token"
					);
				}

A
Asher 已提交
678
				protocol.sendMessage(await ok());
679 680 681

				let connection: Connection;
				if (message.desiredConnectionType === ConnectionType.Management) {
682
					connection = new ManagementConnection(protocol, token);
683
					this._onDidClientConnect.fire({
A
Asher 已提交
684
						protocol, onDidClientDisconnect: connection.onClose,
685
					});
A
Asher 已提交
686 687 688
					// TODO: Need a way to match clients with a connection. For now
					// dispose everything which only works because no extensions currently
					// utilize long-running proxies.
A
Asher 已提交
689 690
					(this.services.get(INodeProxyService) as NodeProxyService)._onUp.fire();
					connection.onClose(() => (this.services.get(INodeProxyService) as NodeProxyService)._onDown.fire());
691
				} else {
A
Asher 已提交
692
					const buffer = protocol.readEntireBuffer();
A
Asher 已提交
693
					connection = new ExtensionHostConnection(
A
Asher 已提交
694
						message.args ? message.args.language : "en",
695
						protocol, buffer, token,
A
Asher 已提交
696 697
						this.services.get(ILogService) as ILogService,
						this.services.get(IEnvironmentService) as IEnvironmentService,
A
Asher 已提交
698
					);
699
				}
A
Asher 已提交
700 701
				connections.set(token, connection);
				connection.onClose(() => connections.delete(token));
702
				this.disposeOldOfflineConnections(connections);
703 704 705 706 707 708
				break;
			case ConnectionType.Tunnel: return protocol.tunnel();
			default: throw new Error("Unrecognized connection type");
		}
	}

709 710 711 712 713 714
	private disposeOldOfflineConnections(connections: Map<string, Connection>): void {
		const offline = Array.from(connections.values())
			.filter((connection) => typeof connection.offline !== "undefined");
		for (let i = 0, max = offline.length - this.maxExtraOfflineConnections; i < max; ++i) {
			offline[i].dispose();
		}
715 716
	}

A
Asher 已提交
717 718 719
	private async initializeServices(args: ParsedArgs): Promise<void> {
		const environmentService = new EnvironmentService(args, process.execPath);
		const logService = new SpdLogService(RemoteExtensionLogFileName, environmentService.logsPath, getLogLevel(environmentService));
A
Asher 已提交
720 721 722
		const fileService = new FileService(logService);
		fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService));

A
Asher 已提交
723 724 725 726 727 728 729 730
		this.allowedRequestPaths.push(
			path.join(environmentService.userDataPath, "clp"), // Language packs.
			environmentService.extensionsPath,
			environmentService.builtinExtensionsPath,
			...environmentService.extraExtensionPaths,
			...environmentService.extraBuiltinExtensionPaths,
		);

A
Asher 已提交
731
		this.ipc.registerChannel("logger", new LoggerChannel(logService));
A
Asher 已提交
732
		this.ipc.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());
A
Asher 已提交
733 734 735 736 737

		this.services.set(ILogService, logService);
		this.services.set(IEnvironmentService, environmentService);
		this.services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.machineSettingsResource]));
		this.services.set(IRequestService, new SyncDescriptor(RequestService));
A
Asher 已提交
738
		this.services.set(IFileService, fileService);
A
Asher 已提交
739
		this.services.set(IProductService, { _serviceBrand: undefined, ...product });
A
Asher 已提交
740
		this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
A
Asher 已提交
741 742
		this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));

A
Asher 已提交
743 744 745 746 747 748 749
		if (!environmentService.args["disable-telemetry"]) {
			this.services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [{
				appender: combinedAppender(
					new AppInsightsAppender("code-server", null, () => new TelemetryClient(), logService),
					new LogAppender(logService),
				),
				commonProperties: resolveCommonProperties(
A
Asher 已提交
750
					product.commit, product.codeServerVersion, await getMachineId(),
A
Asher 已提交
751
					[], environmentService.installSourcePath, "code-server",
A
Asher 已提交
752
				),
A
Asher 已提交
753
				piiPaths: this.allowedRequestPaths,
A
Asher 已提交
754 755 756 757 758
			} as ITelemetryServiceConfig]));
		} else {
			this.services.set(ITelemetryService, NullTelemetryService);
		}

A
Asher 已提交
759 760
		await new Promise((resolve) => {
			const instantiationService = new InstantiationService(this.services);
A
Asher 已提交
761 762 763
			this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
			this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService));

A
Asher 已提交
764 765
			instantiationService.invokeFunction(() => {
				instantiationService.createInstance(LogsDataCleaner);
A
Asher 已提交
766
				const telemetryService = this.services.get(ITelemetryService) as ITelemetryService;
A
Asher 已提交
767 768 769 770 771 772 773 774 775 776 777 778 779
				this.ipc.registerChannel("extensions", new ExtensionManagementChannel(
					this.services.get(IExtensionManagementService) as IExtensionManagementService,
					(context) => getUriTransformer(context.remoteAuthority),
				));
				this.ipc.registerChannel("remoteextensionsenvironment", new ExtensionEnvironmentChannel(
					environmentService, logService, telemetryService, this.options.connectionToken || "",
				));
				this.ipc.registerChannel("request", new RequestChannel(this.services.get(IRequestService) as IRequestService));
				this.ipc.registerChannel("telemetry", new TelemetryChannel(telemetryService));
				this.ipc.registerChannel("nodeProxy", new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService));
				this.ipc.registerChannel("localizations", createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService));
				this.ipc.registerChannel("update", new UpdateChannel(instantiationService.createInstance(UpdateService)));
				this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService));
A
Asher 已提交
780
				resolve(new ErrorTelemetry(telemetryService));
A
Asher 已提交
781 782 783 784
			});
		});
	}

785 786 787 788 789 790
	/**
	 * TODO: implement.
	 */
	private async getDebugPort(): Promise<number | undefined> {
		return undefined;
	}
A
Asher 已提交
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865

	/**
	 * Since we can't pass TLS sockets to children, use this to proxy the socket
	 * and pass a non-TLS socket.
	 */
	private createProxy = async (socket: net.Socket): Promise<net.Socket> => {
		if (!(socket instanceof tls.TLSSocket)) {
			return socket;
		}

		await this.startProxyServer();

		return new Promise((resolve, reject) => {
			const timeout = setTimeout(() => {
				listener.dispose();
				socket.destroy();
				proxy.destroy();
				reject(new Error("TLS socket proxy timed out"));
			}, this.proxyTimeout);

			const listener = this._onProxyConnect.event((connection) => {
				connection.once("data", (data) => {
					if (!socket.destroyed && !proxy.destroyed && data.toString() === id) {
						clearTimeout(timeout);
						listener.dispose();
						[[proxy, socket], [socket, proxy]].forEach(([a, b]) => {
							a.pipe(b);
							a.on("error", () => b.destroy());
							a.on("close", () => b.destroy());
							a.on("end", () => b.end());
						});
						resolve(connection);
					}
				});
			});

			const id = generateUuid();
			const proxy = net.connect(this.proxyPipe);
			proxy.once("connect", () => proxy.write(id));
		});
	}

	private async startProxyServer(): Promise<net.Server> {
		if (!this._proxyServer) {
			this._proxyServer = new Promise(async (resolve) => {
				this.proxyPipe = await this.findFreeSocketPath(this.proxyPipe);
				await mkdirp(tmpdir);
				await rimraf(this.proxyPipe);
				const proxyServer = net.createServer((p) => this._onProxyConnect.fire(p));
				proxyServer.once("listening", resolve);
				proxyServer.listen(this.proxyPipe);
			});
		}
		return this._proxyServer;
	}

	private async findFreeSocketPath(basePath: string, maxTries: number = 100): Promise<string> {
		const canConnect = (path: string): Promise<boolean> => {
			return new Promise((resolve) => {
				const socket = net.connect(path);
				socket.once("error", () => resolve(false));
				socket.once("connect", () => {
					socket.destroy();
					resolve(true);
				});
			});
		};

		let i = 0;
		let path = basePath;
		while (await canConnect(path) && i < maxTries) {
			path = `${basePath}-${++i}`;
		}
		return path;
	}
A
Asher 已提交
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902

	/**
	 * Return the file path for Coder settings.
	 */
	private get settingsPath(): string {
		const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
		return path.join(environment.userDataPath, "coder.json");
	}

	/**
	 * Read settings from the file. On a failure return last known settings and
	 * log a warning.
	 *
	 */
	private async readSettings(): Promise<Settings> {
		try {
			const raw = (await util.promisify(fs.readFile)(this.settingsPath, "utf8")).trim();
			this.settings = raw ? JSON.parse(raw) : {};
		} catch (error) {
			if (error.code !== "ENOENT") {
				(this.services.get(ILogService) as ILogService).warn(error.message);
			}
		}
		return this.settings;
	}

	/**
	 * Write settings combined with current settings. On failure log a warning.
	 */
	private async writeSettings(newSettings: Partial<Settings>): Promise<void> {
		this.settings = { ...this.settings, ...newSettings };
		try {
			await util.promisify(fs.writeFile)(this.settingsPath, JSON.stringify(this.settings));
		} catch (error) {
			(this.services.get(ILogService) as ILogService).warn(error.message);
		}
	}
A
Asher 已提交
903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946

	/**
	 * Return the file path for the heartbeat file.
	 */
	private get heartbeatPath(): string {
		const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
		return path.join(environment.userDataPath, "heartbeat");
	}

	/**
	 * Return all online connections regardless of type.
	 */
	private get onlineConnections(): Connection[] {
		const online = <Connection[]>[];
		this.connections.forEach((connections) => {
			connections.forEach((connection) => {
				if (typeof connection.offline === "undefined") {
					online.push(connection);
				}
			});
		});
		return online;
	}

	/**
	 * Write to the heartbeat file if we haven't already done so within the
	 * timeout and start or reset a timer that keeps running as long as there are
	 * active connections. Failures are logged as warnings.
	 */
	private heartbeat(): void {
		const now = Date.now();
		if (now - this.lastHeartbeat >= this.heartbeatInterval) {
			util.promisify(fs.writeFile)(this.heartbeatPath, "").catch((error) => {
				(this.services.get(ILogService) as ILogService).warn(error.message);
			});
			this.lastHeartbeat = now;
			clearTimeout(this.heartbeatTimer!); // We can clear undefined so ! is fine.
			this.heartbeatTimer = setTimeout(() => {
				if (this.onlineConnections.length > 0) {
					this.heartbeat();
				}
			}, this.heartbeatInterval);
		}
	}
A
Asher 已提交
947
}