diff --git a/scripts/ci.bash b/scripts/ci.bash index b666b3527..354647c06 100755 --- a/scripts/ci.bash +++ b/scripts/ci.bash @@ -17,7 +17,7 @@ function docker-build() { if [[ "${image}" == "codercom/nbin-alpine" ]] ; then docker exec "${containerId}" apk add libxkbfile-dev libsecret-dev else - # TODO: at some point git existing but now it seems to have disappeared. + # TODO: at some point git existed but it seems to have disappeared. docker exec "${containerId}" yum install -y libxkbfile-devel libsecret-devel git fi diff --git a/scripts/vscode.patch b/scripts/vscode.patch index 4159cd590..421c15780 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -371,7 +371,7 @@ index 9f68b645b6..f0cae7111d 100644 this.channel.call('setLevel', level); } diff --git a/src/vs/platform/remote/browser/browserWebSocketFactory.ts b/src/vs/platform/remote/browser/browserWebSocketFactory.ts -index 6d9ecbcf5a..1ebd5a4b84 100644 +index 6d9ecbcf5a..1b3499dddf 100644 --- a/src/vs/platform/remote/browser/browserWebSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserWebSocketFactory.ts @@ -79,7 +79,7 @@ class BrowserSocket implements ISocket { @@ -379,7 +379,7 @@ index 6d9ecbcf5a..1ebd5a4b84 100644 connect(host: string, port: number, query: string, callback: IConnectCallback): void { const errorListener = (err: any) => callback(err, undefined); - const socket = new WebSocket(`ws://${host}:${port}/?${query}&skipWebSocketFrames=false`); -+ const socket = new WebSocket(`ws://${host}:${port}${window.location.pathname.replace(/\/+$/, '')}/?${query}&skipWebSocketFrames=false`); ++ const socket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${host}:${port}${window.location.pathname.replace(/\/+$/, '')}/?${query}&skipWebSocketFrames=false`); socket.onopen = function (event) { socket.removeEventListener('error', errorListener); callback(undefined, new BrowserSocket(socket)); @@ -1397,7 +1397,7 @@ index 306d58f915..58c603ad3d 100644 if (definition.fontCharacter || definition.fontColor) { let body = ''; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index c28adc0ad9..4517c308da 100644 +index c28adc0ad9..3d1adba3d9 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -128,7 +128,7 @@ import 'vs/workbench/services/extensions/browser/extensionService'; @@ -1422,3 +1422,9 @@ index c28adc0ad9..4517c308da 100644 // Output Panel import 'vs/workbench/contrib/output/browser/output.contribution'; +@@ -356,3 +356,5 @@ import 'vs/workbench/contrib/outline/browser/outline.contribution'; + // import 'vs/workbench/contrib/issue/electron-browser/issue.contribution'; + + //#endregion ++ ++import 'vs/server/src/client'; diff --git a/src/cli.ts b/src/cli.ts index 4796461a4..8f56ade53 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,9 +7,9 @@ import { buildHelpMessage, buildVersionMessage, options } from "vs/platform/envi import pkg from "vs/platform/product/node/package"; import product from "vs/platform/product/node/product"; -import { AuthType, MainServer } from "vs/server/src/server"; +import { MainServer } from "vs/server/src/server"; import "vs/server/src/tar"; -import { buildAllowedMessage, generateCertificate, generatePassword, open, unpackExecutables } from "vs/server/src/util"; +import { AuthType, buildAllowedMessage, generateCertificate, generatePassword, open, unpackExecutables } from "vs/server/src/util"; interface Args extends ParsedArgs { auth?: AuthType; diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 000000000..7e6040a2d --- /dev/null +++ b/src/client.ts @@ -0,0 +1 @@ +import 'vs/css!./media/firefox'; diff --git a/src/connection.ts b/src/connection.ts index 974e8530e..e4437d11a 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,4 +1,5 @@ import * as cp from "child_process"; +import * as tls from "tls"; import { getPathFromAmdModule } from "vs/base/common/amd"; import { VSBuffer } from "vs/base/common/buffer"; @@ -88,7 +89,7 @@ export class ExtensionHostConnection extends Connection { type: "VSCODE_EXTHOST_IPC_SOCKET", initialDataChunk: (buffer.buffer as Buffer).toString("base64"), skipWebSocketFrames: this.protocol.getSocket() instanceof NodeSocket, - }, socket); + }, socket instanceof tls.TLSSocket ? (socket)._parent : socket); } private spawn(buffer: VSBuffer): cp.ChildProcess { diff --git a/src/media/firefox.css b/src/media/firefox.css new file mode 100644 index 000000000..6ad0811b6 --- /dev/null +++ b/src/media/firefox.css @@ -0,0 +1,21 @@ +@supports (-moz-appearance:none) { + /* + .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button.monaco-text-button { + max-width: 100%; + width: auto; + } + + .monaco-shell .screen-reader-detected-explanation .buttons a, + .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink, + .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button { + max-width: -moz-fit-content; + } + + .monaco-workbench > .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit, + .explorer-viewlet .panel-header .count, + .extensions-viewlet > .extensions .extension > .details > .header-container > .header > .version, + .debug-viewlet .debug-call-stack .stack-frame .label { + min-width: -moz-fit-content; + } + */ +} diff --git a/src/protocol.ts b/src/protocol.ts index 8515b2bc0..138b9c3a4 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -1,4 +1,3 @@ -import * as crypto from "crypto"; import * as net from "net"; import { VSBuffer } from "vs/base/common/buffer"; @@ -13,30 +12,12 @@ export interface SocketOptions { } export class Protocol extends PersistentProtocol { - public constructor( - secWebsocketKey: string, - socket: net.Socket, - public readonly options: SocketOptions, - ) { + public constructor(socket: net.Socket, public readonly options: SocketOptions) { super( options.skipWebSocketFrames ? new NodeSocket(socket) : new WebSocketNodeSocket(new NodeSocket(socket)), ); - socket.on("error", () => socket.destroy()); - socket.on("end", () => socket.destroy()); - - // This magic value is specified by the websocket spec. - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - const reply = crypto.createHash("sha1") - .update(secWebsocketKey + 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"); } public getUnderlyingSocket(): net.Socket { diff --git a/src/server.ts b/src/server.ts index 6682c891b..72d0c94e1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,3 +1,4 @@ +import * as crypto from "crypto"; import * as fs from "fs"; import * as http from "http"; import * as https from "https"; @@ -55,7 +56,7 @@ import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/se import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/src/channel"; import { TelemetryClient } from "vs/server/src/insights"; import { Protocol } from "vs/server/src/protocol"; -import { getMediaMime, getUriTransformer } from "vs/server/src/util"; +import { AuthType, getMediaMime, getUriTransformer } from "vs/server/src/util"; export enum HttpCode { Ok = 200, @@ -95,10 +96,6 @@ export class HttpError extends Error { } } -export enum AuthType { - Password = "password", -} - export interface ServerOptions { readonly auth?: AuthType; readonly basePath?: string; @@ -140,6 +137,7 @@ export abstract class Server { if (!this.listenPromise) { this.listenPromise = new Promise((resolve, reject) => { this.server.on("error", reject); + this.server.on("upgrade", this.onUpgrade); const onListen = () => resolve(this.address()); if (this.options.socket) { this.server.listen(this.options.socket, onListen); @@ -167,6 +165,11 @@ export abstract class Server { return `${this.protocol}://${endpoint}`; } + protected abstract handleWebSocket( + socket: net.Socket, + parsedUrl: url.UrlWithParsedQuery + ): Promise; + protected abstract handleRequest( base: string, requestPath: string, @@ -174,7 +177,8 @@ export abstract class Server { request: http.IncomingMessage, ): Promise; - protected async getResource(filePath: string): Promise { + protected async getResource(...parts: string[]): Promise { + const filePath = path.join(...parts); return { content: await util.promisify(fs.readFile)(filePath), filePath }; } @@ -205,7 +209,7 @@ export abstract class Server { return { redirect: request.url }; } - const parsedUrl = request.url ? url.parse(request.url, true) : {} as url.UrlWithParsedQuery; + const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}}; const fullPath = decodeURIComponent(parsedUrl.pathname || "/"); const match = fullPath.match(/^(\/?[^/]*)(.*)$/); let [, base, requestPath] = match @@ -218,15 +222,13 @@ export abstract class Server { base = "/"; } base = path.normalize(base); - if (requestPath !== "") { // "" will become "." with normalize. - requestPath = path.normalize(requestPath); - } + requestPath = path.normalize(requestPath || "/index.html"); switch (base) { case "/": this.ensureGet(request); if (requestPath === "/favicon.ico") { - return this.getResource(path.join(this.rootPath, "/out/vs/server/src/favicon", requestPath)); + return this.getResource(this.rootPath, "/out/vs/server/src/favicon", requestPath); } else if (!this.authenticate(request)) { return { redirect: "/login" }; } @@ -238,11 +240,11 @@ export abstract class Server { return this.tryLogin(request); } this.ensureGet(request); - return this.getResource(path.join(this.rootPath, "/out/vs/server/src/login", requestPath)); + return this.getResource(this.rootPath, "/out/vs/server/src/login", requestPath); default: this.ensureGet(request); if (!this.authenticate(request)) { - throw new HttpError(`Unauthorized`, HttpCode.Unauthorized); + throw new HttpError("Unauthorized", HttpCode.Unauthorized); } break; } @@ -250,6 +252,41 @@ export abstract class Server { return this.handleRequest(base, requestPath, parsedUrl, request); } + private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket): Promise => { + try { + await this.preHandleWebSocket(request, socket); + } catch (error) { + socket.destroy(); + console.error(error); + } + } + + private preHandleWebSocket(request: http.IncomingMessage, socket: net.Socket): Promise { + socket.on("error", () => socket.destroy()); + socket.on("end", () => socket.destroy()); + + if (!this.authenticate(request)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized); + } else if (request.headers.upgrade !== "websocket") { + 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(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); + } + private async tryLogin(request: http.IncomingMessage): Promise { if (this.authenticate(request)) { this.ensureGet(request); @@ -305,10 +342,7 @@ export abstract class Server { const onData = (d: Buffer): void => { body += d; if (body.length > 1e6) { - onError(new HttpError( - "Payload is too large", - HttpCode.LargePayload, - )); + onError(new HttpError("Payload is too large", HttpCode.LargePayload)); request.connection.destroy(); } }; @@ -359,16 +393,6 @@ export class MainServer extends Server { public constructor(options: ServerOptions, args: ParsedArgs) { super(options); - this.server.on("upgrade", async (request, socket) => { - const protocol = this.createProtocol(request, socket); - try { - await this.connect(await protocol.handshake(), protocol); - } catch (error) { - protocol.sendMessage({ type: "error", reason: error.message }); - protocol.dispose(); - protocol.getSocket().dispose(); - } - }); this.servicesPromise = this.initializeServices(args); } @@ -382,6 +406,21 @@ export class MainServer extends Server { return address; } + protected async handleWebSocket(socket: net.Socket, parsedUrl: url.UrlWithParsedQuery): Promise { + const protocol = new Protocol(socket, { + reconnectionToken: parsedUrl.query.reconnectionToken || "", + 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(); + } + } + protected async handleRequest( base: string, requestPath: string, @@ -390,14 +429,15 @@ export class MainServer extends Server { ): Promise { switch (base) { case "/": return this.getRoot(request, parsedUrl); - case "/node_modules": - case "/out": - return this.getResource(path.join(this.rootPath, base, requestPath)); case "/resources": return this.getResource(requestPath); case "/webview": - const webviewPath = path.join(this.rootPath, "out/vs/workbench/contrib/webview/browser/pre"); - return this.getResource(path.join(webviewPath, requestPath || "/index.html")); - default: throw new HttpError("Not found", HttpCode.NotFound); + return this.getResource( + this.rootPath, + "out/vs/workbench/contrib/webview/browser/pre", + requestPath + ); + default: + return this.getResource(this.rootPath, base, requestPath); } } @@ -440,18 +480,6 @@ export class MainServer extends Server { return { content, filePath }; } - private createProtocol(request: http.IncomingMessage, socket: net.Socket): Protocol { - if (request.headers.upgrade !== "websocket") { - throw new Error("HTTP/1.1 400 Bad Request"); - } - const query = request.url ? url.parse(request.url, true).query : {}; - return new Protocol(request.headers["sec-websocket-key"], socket, { - reconnectionToken: query.reconnectionToken || "", - reconnection: query.reconnection === "true", - skipWebSocketFrames: query.skipWebSocketFrames === "true", - }); - } - private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise { switch (message.desiredConnectionType) { case ConnectionType.ExtensionHost: diff --git a/src/util.ts b/src/util.ts index 1c51ab33b..4d5f64977 100644 --- a/src/util.ts +++ b/src/util.ts @@ -12,7 +12,9 @@ import { extname } from "vs/base/common/path"; import { URITransformer, IRawURITransformer } from "vs/base/common/uriIpc"; import { mkdirp } from "vs/base/node/pfs"; -import { AuthType } from "vs/server/src/server"; +export enum AuthType { + Password = "password", +} export const tmpdir = path.join(os.tmpdir(), "code-server");