From 92bf2c976068b513256a909cbe8822e460c96524 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 10:16:01 -0500 Subject: [PATCH 01/18] Add dev mode constant --- src/node/constants.ts | 1 + src/node/routes/vscode.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/constants.ts b/src/node/constants.ts index c198f8fb..d36f9a24 100644 --- a/src/node/constants.ts +++ b/src/node/constants.ts @@ -20,3 +20,4 @@ export const version = pkg.version || "development" export const commit = pkg.commit || "development" export const rootPath = path.resolve(__dirname, "../..") export const tmpdir = path.join(os.tmpdir(), "code-server") +export const isDevMode = commit === "development" diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 0d9c6309..287216ec 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -7,7 +7,7 @@ import * as ipc from "../../../typings/ipc" import { Emitter } from "../../common/emitter" import { HttpCode, HttpError } from "../../common/http" import { getFirstString } from "../../common/util" -import { commit, rootPath, version } from "../constants" +import { isDevMode, rootPath, version } from "../constants" import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" import { getMediaMime, pathToFsPath } from "../util" import { VscodeProvider } from "../vscode" @@ -31,7 +31,7 @@ router.get("/", async (req, res) => { try { return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query) } catch (error) { - const devMessage = commit === "development" ? "It might not have finished compiling." : "" + const devMessage = isDevMode ? "It might not have finished compiling." : "" throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) } })(), @@ -44,7 +44,7 @@ router.get("/", async (req, res) => { req, // Uncomment prod blocks if not in development. TODO: Would this be // better as a build step? Or maintain two HTML files again? - commit !== "development" ? content.replace(//g, "") : content, + !isDevMode ? content.replace(//g, "") : content, { authed: req.args.auth !== "none", disableTelemetry: !!req.args["disable-telemetry"], From 083400b50a2983c05f81a41f38afd2c61350ed08 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 4 May 2021 16:46:08 -0500 Subject: [PATCH 02/18] Add flag to enable permessage-deflate --- src/node/cli.ts | 9 +++++++++ src/node/entry.ts | 16 ++++++++++++++++ src/node/routes/vscode.ts | 14 +++++++++++--- test/unit/cli.test.ts | 5 +++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 461a2e18..8278dfed 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -6,6 +6,11 @@ import * as path from "path" import { Args as VsArgs } from "../../typings/ipc" import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" +export enum Feature { + /** Web socket compression. */ + PermessageDeflate = "permessage-deflate", +} + export enum AuthType { Password = "password", None = "none", @@ -35,6 +40,7 @@ export interface Args extends VsArgs { "cert-key"?: string "disable-telemetry"?: boolean "disable-update-check"?: boolean + enable?: string[] help?: boolean host?: string json?: boolean @@ -128,6 +134,9 @@ const options: Options> = { "Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" + "then notifies you once every week that a new release is available.", }, + // --enable can be used to enable experimental features. These features + // provide no guarantees. + enable: { type: "string[]" }, help: { type: "boolean", short: "h", description: "Show this output." }, json: { type: "boolean" }, open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." }, diff --git a/src/node/entry.ts b/src/node/entry.ts index b7b61b8f..12d74da3 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -8,6 +8,7 @@ import { createApp, ensureAddress } from "./app" import { AuthType, DefaultedArgs, + Feature, optionDescriptions, parse, readConfigFile, @@ -146,6 +147,21 @@ const main = async (args: DefaultedArgs): Promise => { } } + if (args.enable && args.enable.length > 0) { + logger.info("Enabling the following experimental features:") + args.enable.forEach((feature) => { + if (Object.values(Feature).includes(feature as Feature)) { + logger.info(` - "${feature}"`) + } else { + logger.error(` X "${feature}" (unknown feature)`) + } + }) + // TODO: Could be nice to add wrapping to the logger? + logger.info( + " The code-server project does not provide stability guarantees or commit to fixing bugs relating to these experimental features. When filing bug reports, please ensure that you can reproduce the bug with all experimental features turned off.", + ) + } + if (!args.socket && args.open) { // The web socket doesn't seem to work if browsing with 0.0.0.0. const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 287216ec..40c63e92 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -7,6 +7,7 @@ import * as ipc from "../../../typings/ipc" import { Emitter } from "../../common/emitter" import { HttpCode, HttpError } from "../../common/http" import { getFirstString } from "../../common/util" +import { Feature } from "../cli" import { isDevMode, rootPath, version } from "../constants" import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" import { getMediaMime, pathToFsPath } from "../util" @@ -209,14 +210,21 @@ wsRouter.ws("/", ensureAuthenticated, async (req) => { `Sec-WebSocket-Accept: ${reply}`, ] + // See if the browser reports it supports web socket compression. // TODO: Parse this header properly. const extensions = req.headers["sec-websocket-extensions"] - const permessageDeflate = extensions ? extensions.includes("permessage-deflate") : false - if (permessageDeflate) { + const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false + + // TODO: For now we only use compression if the user enables it. + const isCompressionEnabled = !!req.args.enable?.includes(Feature.PermessageDeflate) + + const useCompression = isCompressionEnabled && isCompressionSupported + if (useCompression) { + // This response header tells the browser the server supports compression. responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15") } req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n") - await vscode.sendWebsocket(req.ws, req.query, permessageDeflate) + await vscode.sendWebsocket(req.ws, req.query, useCompression) }) diff --git a/test/unit/cli.test.ts b/test/unit/cli.test.ts index 38d8dc7b..6d4fcafb 100644 --- a/test/unit/cli.test.ts +++ b/test/unit/cli.test.ts @@ -39,6 +39,10 @@ describe("parser", () => { it("should parse all available options", () => { expect( parse([ + "--enable", + "feature1", + "--enable", + "feature2", "--bind-addr=192.169.0.1:8080", "--auth", "none", @@ -82,6 +86,7 @@ describe("parser", () => { cert: { value: path.resolve("baz"), }, + enable: ["feature1", "feature2"], "extensions-dir": path.resolve("foo"), "extra-builtin-extensions-dir": [path.resolve("bazzle")], "extra-extensions-dir": [path.resolve("nozzle")], From c96fb65308bf8ff6749457abf331fa8417647704 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 11:51:09 -0500 Subject: [PATCH 03/18] Split some entry methods into main This is so they can be unit tested. --- src/node/entry.ts | 168 +--------------------------------------------- src/node/main.ts | 163 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 165 deletions(-) create mode 100644 src/node/main.ts diff --git a/src/node/entry.ts b/src/node/entry.ts index 12d74da3..24a557e1 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,14 +1,5 @@ -import { field, logger } from "@coder/logger" -import * as cp from "child_process" -import http from "http" -import * as path from "path" -import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc" -import { plural } from "../common/util" -import { createApp, ensureAddress } from "./app" +import { logger } from "@coder/logger" import { - AuthType, - DefaultedArgs, - Feature, optionDescriptions, parse, readConfigFile, @@ -16,164 +7,11 @@ import { shouldOpenInExistingInstance, shouldRunVsCodeCli, } from "./cli" -import { coderCloudBind } from "./coder_cloud" import { commit, version } from "./constants" +import { openInExistingInstance, runCodeServer, runVsCodeCli } from "./main" import * as proxyAgent from "./proxy_agent" -import { register } from "./routes" -import { humanPath, isFile, open } from "./util" import { isChild, wrapper } from "./wrapper" -export const runVsCodeCli = (args: DefaultedArgs): void => { - logger.debug("forking vs code cli...") - const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { - env: { - ...process.env, - CODE_SERVER_PARENT_PID: process.pid.toString(), - }, - }) - vscode.once("message", (message: any) => { - logger.debug("got message from VS Code", field("message", message)) - if (message.type !== "ready") { - logger.error("Unexpected response waiting for ready response", field("type", message.type)) - process.exit(1) - } - const send: CliMessage = { type: "cli", args } - vscode.send(send) - }) - vscode.once("error", (error) => { - logger.error("Got error from VS Code", field("error", error)) - process.exit(1) - }) - vscode.on("exit", (code) => process.exit(code || 0)) -} - -export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise => { - const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { - type: "open", - folderURIs: [], - fileURIs: [], - forceReuseWindow: args["reuse-window"], - forceNewWindow: args["new-window"], - } - - for (let i = 0; i < args._.length; i++) { - const fp = path.resolve(args._[i]) - if (await isFile(fp)) { - pipeArgs.fileURIs.push(fp) - } else { - pipeArgs.folderURIs.push(fp) - } - } - - if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) { - logger.error("--new-window can only be used with folder paths") - process.exit(1) - } - - if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) { - logger.error("Please specify at least one file or folder") - process.exit(1) - } - - const vscode = http.request( - { - path: "/", - method: "POST", - socketPath, - }, - (response) => { - response.on("data", (message) => { - logger.debug("got message from VS Code", field("message", message.toString())) - }) - }, - ) - vscode.on("error", (error: unknown) => { - logger.error("got error from VS Code", field("error", error)) - }) - vscode.write(JSON.stringify(pipeArgs)) - vscode.end() -} - -const main = async (args: DefaultedArgs): Promise => { - logger.info(`code-server ${version} ${commit}`) - - logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) - logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) - - if (args.auth === AuthType.Password && !args.password && !args["hashed-password"]) { - throw new Error( - "Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)", - ) - } - - const [app, wsApp, server] = await createApp(args) - const serverAddress = ensureAddress(server) - await register(app, wsApp, server, args) - - logger.info(`Using config file ${humanPath(args.config)}`) - logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) - - if (args.auth === AuthType.Password) { - logger.info(" - Authentication is enabled") - if (args.usingEnvPassword) { - logger.info(" - Using password from $PASSWORD") - } else if (args.usingEnvHashedPassword) { - logger.info(" - Using password from $HASHED_PASSWORD") - } else { - logger.info(` - Using password from ${humanPath(args.config)}`) - } - } else { - logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) - } - - if (args.cert) { - logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`) - } else { - logger.info(` - Not serving HTTPS ${args.link ? "(disabled by --link)" : ""}`) - } - - if (args["proxy-domain"].length > 0) { - logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`) - args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`)) - } - - if (args.link) { - try { - await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) - logger.info(" - Connected to cloud agent") - } catch (err) { - logger.error(err.message) - wrapper.exit(1) - } - } - - if (args.enable && args.enable.length > 0) { - logger.info("Enabling the following experimental features:") - args.enable.forEach((feature) => { - if (Object.values(Feature).includes(feature as Feature)) { - logger.info(` - "${feature}"`) - } else { - logger.error(` X "${feature}" (unknown feature)`) - } - }) - // TODO: Could be nice to add wrapping to the logger? - logger.info( - " The code-server project does not provide stability guarantees or commit to fixing bugs relating to these experimental features. When filing bug reports, please ensure that you can reproduce the bug with all experimental features turned off.", - ) - } - - if (!args.socket && args.open) { - // The web socket doesn't seem to work if browsing with 0.0.0.0. - const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") - try { - await open(openAddress) - logger.info(`Opened ${openAddress}`) - } catch (error) { - logger.error("Failed to open", field("address", openAddress), field("error", error)) - } - } -} - async function entry(): Promise { proxyAgent.monkeyPatch(false) @@ -186,7 +24,7 @@ async function entry(): Promise { if (isChild(wrapper)) { const args = await wrapper.handshake() wrapper.preventExit() - return main(args) + return runCodeServer(args) } const cliArgs = parse(process.argv.slice(2)) diff --git a/src/node/main.ts b/src/node/main.ts new file mode 100644 index 00000000..240831c2 --- /dev/null +++ b/src/node/main.ts @@ -0,0 +1,163 @@ +import { field, logger } from "@coder/logger" +import * as cp from "child_process" +import http from "http" +import * as path from "path" +import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc" +import { plural } from "../common/util" +import { createApp, ensureAddress } from "./app" +import { AuthType, DefaultedArgs, Feature } from "./cli" +import { coderCloudBind } from "./coder_cloud" +import { commit, version } from "./constants" +import { register } from "./routes" +import { humanPath, isFile, open } from "./util" +import { wrapper } from "./wrapper" + +export const runVsCodeCli = (args: DefaultedArgs): void => { + logger.debug("forking vs code cli...") + const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { + env: { + ...process.env, + CODE_SERVER_PARENT_PID: process.pid.toString(), + }, + }) + vscode.once("message", (message: any) => { + logger.debug("got message from VS Code", field("message", message)) + if (message.type !== "ready") { + logger.error("Unexpected response waiting for ready response", field("type", message.type)) + process.exit(1) + } + const send: CliMessage = { type: "cli", args } + vscode.send(send) + }) + vscode.once("error", (error) => { + logger.error("Got error from VS Code", field("error", error)) + process.exit(1) + }) + vscode.on("exit", (code) => process.exit(code || 0)) +} + +export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise => { + const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { + type: "open", + folderURIs: [], + fileURIs: [], + forceReuseWindow: args["reuse-window"], + forceNewWindow: args["new-window"], + } + + for (let i = 0; i < args._.length; i++) { + const fp = path.resolve(args._[i]) + if (await isFile(fp)) { + pipeArgs.fileURIs.push(fp) + } else { + pipeArgs.folderURIs.push(fp) + } + } + + if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) { + logger.error("--new-window can only be used with folder paths") + process.exit(1) + } + + if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) { + logger.error("Please specify at least one file or folder") + process.exit(1) + } + + const vscode = http.request( + { + path: "/", + method: "POST", + socketPath, + }, + (response) => { + response.on("data", (message) => { + logger.debug("got message from VS Code", field("message", message.toString())) + }) + }, + ) + vscode.on("error", (error: unknown) => { + logger.error("got error from VS Code", field("error", error)) + }) + vscode.write(JSON.stringify(pipeArgs)) + vscode.end() +} + +export const runCodeServer = async (args: DefaultedArgs): Promise => { + logger.info(`code-server ${version} ${commit}`) + + logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) + logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) + + if (args.auth === AuthType.Password && !args.password && !args["hashed-password"]) { + throw new Error( + "Please pass in a password via the config file or environment variable ($PASSWORD or $HASHED_PASSWORD)", + ) + } + + const [app, wsApp, server] = await createApp(args) + const serverAddress = ensureAddress(server) + await register(app, wsApp, server, args) + + logger.info(`Using config file ${humanPath(args.config)}`) + logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) + if (args.auth === AuthType.Password) { + logger.info(" - Authentication is enabled") + if (args.usingEnvPassword) { + logger.info(" - Using password from $PASSWORD") + } else if (args.usingEnvHashedPassword) { + logger.info(" - Using password from $HASHED_PASSWORD") + } else { + logger.info(` - Using password from ${humanPath(args.config)}`) + } + } else { + logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) + } + + if (args.cert) { + logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`) + } else { + logger.info(` - Not serving HTTPS ${args.link ? "(disabled by --link)" : ""}`) + } + + if (args["proxy-domain"].length > 0) { + logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`) + args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`)) + } + + if (args.link) { + try { + await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) + logger.info(" - Connected to cloud agent") + } catch (err) { + logger.error(err.message) + wrapper.exit(1) + } + } + + if (args.enable && args.enable.length > 0) { + logger.info("Enabling the following experimental features:") + args.enable.forEach((feature) => { + if (Object.values(Feature).includes(feature as Feature)) { + logger.info(` - "${feature}"`) + } else { + logger.error(` X "${feature}" (unknown feature)`) + } + }) + // TODO: Could be nice to add wrapping to the logger? + logger.info( + " The code-server project does not provide stability guarantees or commit to fixing bugs relating to these experimental features. When filing bug reports, please ensure that you can reproduce the bug with all experimental features turned off.", + ) + } + + if (!args.socket && args.open) { + // The web socket doesn't seem to work if browsing with 0.0.0.0. + const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") + try { + await open(openAddress) + logger.info(`Opened ${openAddress}`) + } catch (error) { + logger.error("Failed to open", field("address", openAddress), field("error", error)) + } + } +} From 20e70cfa053e5f3847ab7409e5cc9abd8f0c8187 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 11:56:19 -0500 Subject: [PATCH 04/18] Remove try from main All it does is log and exit which is what the caller will be doing on an error anyway (see entry). --- src/node/main.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/node/main.ts b/src/node/main.ts index 240831c2..1409a440 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -10,7 +10,6 @@ import { coderCloudBind } from "./coder_cloud" import { commit, version } from "./constants" import { register } from "./routes" import { humanPath, isFile, open } from "./util" -import { wrapper } from "./wrapper" export const runVsCodeCli = (args: DefaultedArgs): void => { logger.debug("forking vs code cli...") @@ -126,13 +125,8 @@ export const runCodeServer = async (args: DefaultedArgs): Promise => { } if (args.link) { - try { - await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) - logger.info(" - Connected to cloud agent") - } catch (err) { - logger.error(err.message) - wrapper.exit(1) - } + await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) + logger.info(" - Connected to cloud agent") } if (args.enable && args.enable.length > 0) { From a882be574887367b378658535d2414e09e591ad5 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 12:20:38 -0500 Subject: [PATCH 05/18] Refactor integration tests to use main entry point --- src/node/entry.ts | 3 ++- src/node/main.ts | 4 +++- test/unit/health.test.ts | 4 ++-- test/unit/proxy.test.ts | 10 +++++----- test/utils/integration.ts | 16 +++++----------- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 24a557e1..4b186fef 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -24,7 +24,8 @@ async function entry(): Promise { if (isChild(wrapper)) { const args = await wrapper.handshake() wrapper.preventExit() - return runCodeServer(args) + await runCodeServer(args) + return } const cliArgs = parse(process.argv.slice(2)) diff --git a/src/node/main.ts b/src/node/main.ts index 1409a440..e0036413 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -82,7 +82,7 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st vscode.end() } -export const runCodeServer = async (args: DefaultedArgs): Promise => { +export const runCodeServer = async (args: DefaultedArgs): Promise => { logger.info(`code-server ${version} ${commit}`) logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) @@ -154,4 +154,6 @@ export const runCodeServer = async (args: DefaultedArgs): Promise => { logger.error("Failed to open", field("address", openAddress), field("error", error)) } } + + return server } diff --git a/test/unit/health.test.ts b/test/unit/health.test.ts index a8bdfb19..2c5fc0b7 100644 --- a/test/unit/health.test.ts +++ b/test/unit/health.test.ts @@ -12,7 +12,7 @@ describe("health", () => { }) it("/healthz", async () => { - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch("/healthz") expect(resp.status).toBe(200) const json = await resp.json() @@ -20,7 +20,7 @@ describe("health", () => { }) it("/healthz (websocket)", async () => { - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const ws = codeServer.ws("/healthz") const message = await new Promise((resolve, reject) => { ws.on("error", console.error) diff --git a/test/unit/proxy.test.ts b/test/unit/proxy.test.ts index c5969ebf..a9c1554e 100644 --- a/test/unit/proxy.test.ts +++ b/test/unit/proxy.test.ts @@ -37,7 +37,7 @@ describe("proxy", () => { e.get("/wsup", (req, res) => { res.json("asher is the best") }) - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath) expect(resp.status).toBe(200) const json = await resp.json() @@ -48,7 +48,7 @@ describe("proxy", () => { e.get(absProxyPath, (req, res) => { res.json("joe is the best") }) - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(absProxyPath) expect(resp.status).toBe(200) const json = await resp.json() @@ -62,7 +62,7 @@ describe("proxy", () => { e.post("/finale", (req, res) => { res.json("redirect success") }) - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath, { method: "POST", }) @@ -78,7 +78,7 @@ describe("proxy", () => { e.post(finalePath, (req, res) => { res.json("redirect success") }) - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(absProxyPath, { method: "POST", }) @@ -91,7 +91,7 @@ describe("proxy", () => { e.post("/wsup", (req, res) => { res.json(req.body) }) - ;[, , codeServer] = await integration.setup(["--auth=none"], "") + codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath, { method: "post", body: JSON.stringify("coder is the best"), diff --git a/test/utils/integration.ts b/test/utils/integration.ts index c5101d0f..1ad7b44d 100644 --- a/test/utils/integration.ts +++ b/test/utils/integration.ts @@ -1,21 +1,15 @@ -import * as express from "express" -import { createApp } from "../../src/node/app" -import { parse, setDefaults, parseConfigFile, DefaultedArgs } from "../../src/node/cli" -import { register } from "../../src/node/routes" +import { parse, parseConfigFile, setDefaults } from "../../src/node/cli" +import { runCodeServer } from "../../src/node/main" import * as httpserver from "./httpserver" -export async function setup( - argv: string[], - configFile?: string, -): Promise<[express.Application, express.Application, httpserver.HttpServer, DefaultedArgs]> { +export async function setup(argv: string[], configFile?: string): Promise { argv = ["--bind-addr=localhost:0", ...argv] const cliArgs = parse(argv) const configArgs = parseConfigFile(configFile || "", "test/integration.ts") const args = await setDefaults(cliArgs, configArgs) - const [app, wsApp, server] = await createApp(args) - await register(app, wsApp, server, args) + const server = await runCodeServer(args) - return [app, wsApp, new httpserver.HttpServer(server), args] + return new httpserver.HttpServer(server) } From 7871cced96ac13d186adc1f1497e4b3b8b45209b Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 4 May 2021 14:49:38 -0700 Subject: [PATCH 06/18] docs(security): add section for tools --- docs/SECURITY.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index bb24654f..c4e24ddf 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,12 +1,28 @@ # Security Policy +The code-server team (and Coder, the organization) care a lot about keeping the project secure and safe for end-users. + +## Tools + +We use a combination of tools to help us stay on top of vulnerabilities. + +- [dependabot](https://dependabot.com/) + - Submits pull requests to upgrade dependencies. We use dependabot's version upgrades as well as security updates. +- code-scanning + - [CodeQL](https://securitylab.github.com/tools/codeql/) + - Semantic code analysis engine that runs on a regular schedule (see `codeql-analysis.yml`) + - [trivy](https://github.com/aquasecurity/trivy) + - Comprehensive vulnerability scanner that runs on PRs into the default branch and scans both our container image and repository code (see `trivy-scan-repo` and `trivy-scan-image` jobs in `ci.yaml`) +- [`audit-ci`](https://github.com/IBM/audit-ci) + - Audits npm and Yarn dependencies in CI (see "Audit for vulnerabilities" step in `ci.yaml`) on PRs into the default branch and fails CI if moderate or higher vulnerabilities(see the `audit.sh` script) are present. + ## Supported Versions Coder sponsors development and maintenance of the code-server project. We will fix security issues within 90 days of receiving a report, and publish the fix in a subsequent release. The code-server project does not provide backports or patch releases for security issues at this time. -| Version | Supported | -| ------- | ------------------ | -| 3.9.3 | :white_check_mark: | +| Version | Supported | +| ----------------------------------------------------- | ------------------ | +| [Latest](https://github.com/cdr/code-server/releases) | :white_check_mark: | ## Reporting a Vulnerability From 027106a5e1f48cc2732ffbccc41f7c1c379f6945 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 4 May 2021 16:55:09 -0700 Subject: [PATCH 07/18] feat(testing): add test for constants "version" and commit --- test/unit/constants.test.ts | 100 +++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/test/unit/constants.test.ts b/test/unit/constants.test.ts index e0733823..f8980f2b 100644 --- a/test/unit/constants.test.ts +++ b/test/unit/constants.test.ts @@ -1,5 +1,4 @@ import * as fs from "fs" -import { commit, getPackageJson, version } from "../../src/node/constants" import { tmpdir } from "../../test/utils/constants" import { loggerModule } from "../utils/helpers" @@ -8,12 +7,14 @@ jest.mock("@coder/logger", () => require("../utils/helpers").loggerModule) describe("constants", () => { describe("getPackageJson", () => { + const { getPackageJson } = require("../../src/node/constants") afterEach(() => { jest.clearAllMocks() }) afterAll(() => { jest.restoreAllMocks() + jest.resetModules() }) it("should log a warning if package.json not found", () => { @@ -36,20 +37,95 @@ describe("constants", () => { }) }) describe("version", () => { - it("should return the package.json version", () => { - // Source: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39#gistcomment-2896416 - const validSemVar = new RegExp("^(0|[1-9]d*).(0|[1-9]d*).(0|[1-9]d*)") - const isValidSemVar = validSemVar.test(version) - expect(version).not.toBe(null) - expect(isValidSemVar).toBe(true) + describe("with package.json.version defined", () => { + let mockPackageJson = { + name: "mock-code-server", + version: "1.0.0", + } + let version = "" + + beforeEach(() => { + jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) + version = require("../../src/node/constants").version + }) + + afterEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + it("should return the package.json version", () => { + // Source: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39#gistcomment-2896416 + const validSemVar = new RegExp("^(0|[1-9]d*).(0|[1-9]d*).(0|[1-9]d*)") + const isValidSemVar = validSemVar.test(version) + expect(version).not.toBe(null) + expect(isValidSemVar).toBe(true) + expect(version).toBe("1.0.0") + }) + }) + describe("with package.json.version missing", () => { + let mockPackageJson = { + name: "mock-code-server", + } + let version = "" + + beforeEach(() => { + jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) + version = require("../../src/node/constants").version + }) + + afterEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + it("should return 'development'", () => { + expect(version).toBe("development") + }) }) }) - describe("commit", () => { - it("should return 'development' if commit is undefined", () => { - // In development, the commit is not stored in our package.json - // But when we build code-server and release it, it is - expect(commit).toBe("development") + describe("with package.json.commit defined", () => { + let mockPackageJson = { + name: "mock-code-server", + commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b", + } + let commit = "" + + beforeEach(() => { + jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) + commit = require("../../src/node/constants").commit + }) + + afterEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + it("should return the package.json.commit", () => { + // Source: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39#gistcomment-2896416 + expect(commit).toBe("f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b") + }) + }) + describe("with package.json.commit missing", () => { + let mockPackageJson = { + name: "mock-code-server", + } + let commit = "" + + beforeEach(() => { + jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) + commit = require("../../src/node/constants").commit + }) + + afterEach(() => { + jest.resetAllMocks() + jest.resetModules() + }) + + it("should return 'development'", () => { + expect(commit).toBe("development") + }) }) }) }) From cb5ab48d4886a92a99b54c8591e6ce67d2476f84 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 5 May 2021 16:38:54 -0700 Subject: [PATCH 08/18] fix: coveragePathIgnorePatterns to /out We were accidentally ignoring `node/routes` because we had "out" instead of "/out" in `coveragePathIgnorePatterns` which caused us to not collect coverage for those files. Now we do. --- package.json | 2 +- test/unit/constants.test.ts | 158 ++++++++++++++---------------------- 2 files changed, 60 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 300b49e5..1a51759b 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "clover" ], "coveragePathIgnorePatterns": [ - "out" + "/out" ], "coverageThreshold": { "global": { diff --git a/test/unit/constants.test.ts b/test/unit/constants.test.ts index f8980f2b..d7d3c66b 100644 --- a/test/unit/constants.test.ts +++ b/test/unit/constants.test.ts @@ -6,126 +6,86 @@ import { loggerModule } from "../utils/helpers" jest.mock("@coder/logger", () => require("../utils/helpers").loggerModule) describe("constants", () => { - describe("getPackageJson", () => { + beforeAll(() => { + jest.clearAllMocks() + jest.resetModules() + }) + describe("with package.json defined", () => { const { getPackageJson } = require("../../src/node/constants") - afterEach(() => { - jest.clearAllMocks() + let mockPackageJson = { + name: "mock-code-server", + description: "Run VS Code on a remote server.", + repository: "https://github.com/cdr/code-server", + version: "1.0.0", + commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b", + } + let version = "" + let commit = "" + + beforeEach(() => { + jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) + commit = require("../../src/node/constants").commit + version = require("../../src/node/constants").version }) afterAll(() => { - jest.restoreAllMocks() + jest.clearAllMocks() jest.resetModules() }) - it("should log a warning if package.json not found", () => { - const expectedErrorMessage = "Cannot find module './package.json' from 'src/node/constants.ts'" - - getPackageJson("./package.json") - - expect(loggerModule.logger.warn).toHaveBeenCalled() - expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage) + it("should provide the commit", () => { + expect(commit).toBe("f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b") }) - it("should find the package.json", () => { - // the function calls require from src/node/constants - // so to get the root package.json we need to use ../../ - const packageJson = getPackageJson("../../package.json") - expect(Object.keys(packageJson).length).toBeGreaterThan(0) - expect(packageJson.name).toBe("code-server") - expect(packageJson.description).toBe("Run VS Code on a remote server.") - expect(packageJson.repository).toBe("https://github.com/cdr/code-server") + it("should return the package.json version", () => { + expect(version).toBe(mockPackageJson.version) }) - }) - describe("version", () => { - describe("with package.json.version defined", () => { - let mockPackageJson = { - name: "mock-code-server", - version: "1.0.0", - } - let version = "" - beforeEach(() => { - jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) - version = require("../../src/node/constants").version + describe("getPackageJson", () => { + it("should log a warning if package.json not found", () => { + const expectedErrorMessage = "Cannot find module './package.json' from 'src/node/constants.ts'" + + getPackageJson("./package.json") + + expect(loggerModule.logger.warn).toHaveBeenCalled() + expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage) }) - afterEach(() => { - jest.resetAllMocks() - jest.resetModules() - }) - - it("should return the package.json version", () => { - // Source: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39#gistcomment-2896416 - const validSemVar = new RegExp("^(0|[1-9]d*).(0|[1-9]d*).(0|[1-9]d*)") - const isValidSemVar = validSemVar.test(version) - expect(version).not.toBe(null) - expect(isValidSemVar).toBe(true) - expect(version).toBe("1.0.0") - }) - }) - describe("with package.json.version missing", () => { - let mockPackageJson = { - name: "mock-code-server", - } - let version = "" - - beforeEach(() => { - jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) - version = require("../../src/node/constants").version - }) - - afterEach(() => { - jest.resetAllMocks() - jest.resetModules() - }) - - it("should return 'development'", () => { - expect(version).toBe("development") + it("should find the package.json", () => { + // the function calls require from src/node/constants + // so to get the root package.json we need to use ../../ + const packageJson = getPackageJson("../../package.json") + expect(Object.keys(packageJson).length).toBeGreaterThan(0) + expect(packageJson.name).toBe("mock-code-server") + expect(packageJson.description).toBe("Run VS Code on a remote server.") + expect(packageJson.repository).toBe("https://github.com/cdr/code-server") }) }) }) - describe("commit", () => { - describe("with package.json.commit defined", () => { - let mockPackageJson = { - name: "mock-code-server", - commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b", - } - let commit = "" - beforeEach(() => { - jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) - commit = require("../../src/node/constants").commit - }) + describe("with incomplete package.json", () => { + let mockPackageJson = { + name: "mock-code-server", + } + let version = "" + let commit = "" - afterEach(() => { - jest.resetAllMocks() - jest.resetModules() - }) - - it("should return the package.json.commit", () => { - // Source: https://gist.github.com/jhorsman/62eeea161a13b80e39f5249281e17c39#gistcomment-2896416 - expect(commit).toBe("f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b") - }) + beforeEach(() => { + jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) + version = require("../../src/node/constants").version + commit = require("../../src/node/constants").commit }) - describe("with package.json.commit missing", () => { - let mockPackageJson = { - name: "mock-code-server", - } - let commit = "" - beforeEach(() => { - jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) - commit = require("../../src/node/constants").commit - }) + afterEach(() => { + jest.clearAllMocks() + jest.resetModules() + }) - afterEach(() => { - jest.resetAllMocks() - jest.resetModules() - }) - - it("should return 'development'", () => { - expect(commit).toBe("development") - }) + it("version should return 'development'", () => { + expect(version).toBe("development") + }) + it("commit should return 'development'", () => { + expect(commit).toBe("development") }) }) }) From d27b12bae93418192adc972741753a89fa3a557c Mon Sep 17 00:00:00 2001 From: Akash Satheesan Date: Fri, 7 May 2021 00:32:10 +0530 Subject: [PATCH 09/18] refactor(ci): split audit from prebuild (#3298) Move dependency audits from prebuild to their own jobs, so that an error does not affect the rest of the build/test process. --- .github/workflows/ci.yaml | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8e6344db..832241dc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,10 +43,6 @@ jobs: if: steps.cache-yarn.outputs.cache-hit != 'true' run: yarn --frozen-lockfile - - name: Audit for vulnerabilities - run: yarn _audit - if: success() - - name: Run yarn fmt run: yarn fmt if: success() @@ -63,6 +59,34 @@ jobs: run: yarn coverage if: success() + audit-ci: + name: Run audit-ci + needs: prebuild + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Install Node.js v12 + uses: actions/setup-node@v2 + with: + node-version: "12" + + - name: Fetch dependencies from cache + id: cache-yarn + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: yarn-build-${{ hashFiles('**/yarn.lock') }} + + - name: Install dependencies + if: steps.cache-yarn.outputs.cache-hit != 'true' + run: yarn --frozen-lockfile + + - name: Audit for vulnerabilities + run: yarn _audit + if: success() + build: name: Build needs: prebuild From 0e4672f6b967f8faf6c3112b577098d2ea70e202 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 10:43:54 -0500 Subject: [PATCH 10/18] Move health route tests to routes directory --- test/unit/{ => routes}/health.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename test/unit/{ => routes}/health.test.ts (91%) diff --git a/test/unit/health.test.ts b/test/unit/routes/health.test.ts similarity index 91% rename from test/unit/health.test.ts rename to test/unit/routes/health.test.ts index 2c5fc0b7..81472ced 100644 --- a/test/unit/health.test.ts +++ b/test/unit/routes/health.test.ts @@ -1,5 +1,5 @@ -import * as httpserver from "../utils/httpserver" -import * as integration from "../utils/integration" +import * as httpserver from "../../utils/httpserver" +import * as integration from "../../utils/integration" describe("health", () => { let codeServer: httpserver.HttpServer | undefined From 1eee766bee969028362d15601b6ed8b653d87d94 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 May 2021 19:25:49 +0000 Subject: [PATCH 11/18] chore(deps-dev): bump underscore from 1.8.3 to 1.12.1 in /lib/vscode Bumps [underscore](https://github.com/jashkenas/underscore) from 1.8.3 to 1.12.1. - [Release notes](https://github.com/jashkenas/underscore/releases) - [Commits](https://github.com/jashkenas/underscore/compare/1.8.3...1.12.1) Signed-off-by: dependabot[bot] --- lib/vscode/package.json | 2 +- lib/vscode/yarn.lock | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/vscode/package.json b/lib/vscode/package.json index 8a5ad904..5103a3dc 100644 --- a/lib/vscode/package.json +++ b/lib/vscode/package.json @@ -195,7 +195,7 @@ "tsec": "0.1.4", "typescript": "^4.3.0-dev.20210305", "typescript-formatter": "7.1.0", - "underscore": "^1.8.2", + "underscore": "^1.12.1", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vscode-debugprotocol": "1.46.0", diff --git a/lib/vscode/yarn.lock b/lib/vscode/yarn.lock index c3c3779e..d528adb5 100644 --- a/lib/vscode/yarn.lock +++ b/lib/vscode/yarn.lock @@ -9132,7 +9132,12 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -underscore@^1.8.2, underscore@~1.8.3: +underscore@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + +underscore@~1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= From 52cf2fcf29400ecc1f1ab196920e440cff0aba40 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 11:02:34 -0500 Subject: [PATCH 12/18] Move tmpdir test helper to test helpers file --- test/e2e/terminal.test.ts | 6 +++--- test/unit/constants.test.ts | 15 --------------- test/unit/helpers.test.ts | 14 ++++++++++++++ test/utils/constants.ts | 11 ----------- test/utils/helpers.ts | 16 ++++++++++++++++ 5 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 test/unit/helpers.test.ts diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index f92419c6..15fd5394 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -1,10 +1,10 @@ -import { test, expect } from "@playwright/test" +import { expect, test } from "@playwright/test" import * as cp from "child_process" import * as fs from "fs" -// import { tmpdir } from "os" import * as path from "path" import util from "util" -import { STORAGE, tmpdir } from "../utils/constants" +import { STORAGE } from "../utils/constants" +import { tmpdir } from "../utils/helpers" import { CodeServer } from "./models/CodeServer" test.describe("Integrated Terminal", () => { diff --git a/test/unit/constants.test.ts b/test/unit/constants.test.ts index d7d3c66b..88654457 100644 --- a/test/unit/constants.test.ts +++ b/test/unit/constants.test.ts @@ -1,5 +1,3 @@ -import * as fs from "fs" -import { tmpdir } from "../../test/utils/constants" import { loggerModule } from "../utils/helpers" // jest.mock is hoisted above the imports so we must use `require` here. @@ -89,16 +87,3 @@ describe("constants", () => { }) }) }) - -describe("test constants", () => { - describe("tmpdir", () => { - it("should return a temp directory", async () => { - const testName = "temp-dir" - const pathToTempDir = await tmpdir(testName) - - expect(pathToTempDir).toContain(testName) - - await fs.promises.rmdir(pathToTempDir) - }) - }) -}) diff --git a/test/unit/helpers.test.ts b/test/unit/helpers.test.ts new file mode 100644 index 00000000..74485475 --- /dev/null +++ b/test/unit/helpers.test.ts @@ -0,0 +1,14 @@ +import { promises as fs } from "fs" +import { tmpdir } from "../../test/utils/helpers" + +/** + * This file is for testing test helpers (not core code). + */ +describe("test helpers", () => { + it("should return a temp directory", async () => { + const testName = "temp-dir" + const pathToTempDir = await tmpdir(testName) + expect(pathToTempDir).toContain(testName) + expect(fs.access(pathToTempDir)).resolves.toStrictEqual(undefined) + }) +}) diff --git a/test/utils/constants.ts b/test/utils/constants.ts index a6abd209..ac2250e1 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -1,14 +1,3 @@ -import * as fs from "fs" -import * as os from "os" -import * as path from "path" - export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080" export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab" export const STORAGE = process.env.STORAGE || "" - -export async function tmpdir(testName: string): Promise { - const dir = path.join(os.tmpdir(), "code-server") - await fs.promises.mkdir(dir, { recursive: true }) - - return await fs.promises.mkdtemp(path.join(dir, `test-${testName}-`), { encoding: "utf8" }) -} diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index b4580401..96a772b9 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -1,3 +1,7 @@ +import * as fs from "fs" +import * as os from "os" +import * as path from "path" + export const loggerModule = { field: jest.fn(), level: 2, @@ -9,3 +13,15 @@ export const loggerModule = { warn: jest.fn(), }, } + +/** + * Create a uniquely named temporary directory. + * + * These directories are placed under a single temporary code-server directory. + */ +export async function tmpdir(testName: string): Promise { + const dir = path.join(os.tmpdir(), "code-server") + await fs.promises.mkdir(dir, { recursive: true }) + + return await fs.promises.mkdtemp(path.join(dir, `test-${testName}-`), { encoding: "utf8" }) +} From 1789cd1bcb3e15c73b5a58c84912bbefeb036c2c Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 11:03:04 -0500 Subject: [PATCH 13/18] Move temp test dirs under a `tests` sub-directory This is to match the other tests that create temp directories. It also lets you clean up test temp directories all at once separately from other non-test temporary directories. --- test/utils/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 96a772b9..df84ec1f 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -20,8 +20,8 @@ export const loggerModule = { * These directories are placed under a single temporary code-server directory. */ export async function tmpdir(testName: string): Promise { - const dir = path.join(os.tmpdir(), "code-server") + const dir = path.join(os.tmpdir(), "code-server/tests") await fs.promises.mkdir(dir, { recursive: true }) - return await fs.promises.mkdtemp(path.join(dir, `test-${testName}-`), { encoding: "utf8" }) + return await fs.promises.mkdtemp(path.join(dir, `${testName}-`), { encoding: "utf8" }) } From 4925e97080e9d62acce333e0e42849fa743e5752 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 11:14:51 -0500 Subject: [PATCH 14/18] Add static route tests --- test/unit/routes/static.test.ts | 136 ++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 test/unit/routes/static.test.ts diff --git a/test/unit/routes/static.test.ts b/test/unit/routes/static.test.ts new file mode 100644 index 00000000..13897626 --- /dev/null +++ b/test/unit/routes/static.test.ts @@ -0,0 +1,136 @@ +import { promises as fs } from "fs" +import * as path from "path" +import { tmpdir } from "../../utils/helpers" +import * as httpserver from "../../utils/httpserver" +import * as integration from "../../utils/integration" + +describe("/static", () => { + let _codeServer: httpserver.HttpServer | undefined + function codeServer(): httpserver.HttpServer { + if (!_codeServer) { + throw new Error("tried to use code-server before setting it up") + } + return _codeServer + } + + let testFile: string | undefined + let testFileContent: string | undefined + let nonExistentTestFile: string | undefined + + // The static endpoint expects a commit and then the full path of the file. + // The commit is just for cache busting so we can use anything we want. `-` + // and `development` are specially recognized in that they will cause the + // static endpoint to avoid sending cache headers. + const commit = "-" + + beforeAll(async () => { + const testDir = await tmpdir("static") + testFile = path.join(testDir, "test") + testFileContent = "static file contents" + nonExistentTestFile = path.join(testDir, "i-am-not-here") + await fs.writeFile(testFile, testFileContent) + }) + + afterEach(async () => { + if (_codeServer) { + await _codeServer.close() + _codeServer = undefined + } + }) + + function commonTests() { + it("should return a 404 when a commit and file are not provided", async () => { + const resp = await codeServer().fetch("/static") + expect(resp.status).toBe(404) + + const content = await resp.json() + expect(content).toStrictEqual({ error: "Not Found" }) + }) + + it("should return a 404 when a file is not provided", async () => { + const resp = await codeServer().fetch(`/static/${commit}`) + expect(resp.status).toBe(404) + + const content = await resp.json() + expect(content).toStrictEqual({ error: "Not Found" }) + }) + } + + describe("disabled authentication", () => { + beforeEach(async () => { + _codeServer = await integration.setup(["--auth=none"], "") + }) + + commonTests() + + it("should return a 404 for a nonexistent file", async () => { + const resp = await codeServer().fetch(`/static/${commit}/${nonExistentTestFile}`) + expect(resp.status).toBe(404) + + const content = await resp.json() + expect(content.error).toMatch("ENOENT") + }) + + it("should return a 200 and file contents for an existent file", async () => { + const resp = await codeServer().fetch(`/static/${commit}${testFile}`) + expect(resp.status).toBe(200) + + const content = await resp.text() + expect(content).toStrictEqual(testFileContent) + }) + }) + + describe("enabled authentication", () => { + // Store whatever might be in here so we can restore it afterward. + // TODO: We should probably pass this as an argument somehow instead of + // manipulating the environment. + const previousEnvPassword = process.env.PASSWORD + + beforeEach(async () => { + process.env.PASSWORD = "test" + _codeServer = await integration.setup(["--auth=password"], "") + }) + + afterEach(() => { + process.env.PASSWORD = previousEnvPassword + }) + + commonTests() + + describe("inside code-server root", () => { + it("should return a 404 for a nonexistent file", async () => { + const resp = await codeServer().fetch(`/static/${commit}/${__filename}-does-not-exist`) + expect(resp.status).toBe(404) + + const content = await resp.json() + expect(content.error).toMatch("ENOENT") + }) + + it("should return a 200 and file contents for an existent file", async () => { + const resp = await codeServer().fetch(`/static/${commit}${__filename}`) + expect(resp.status).toBe(200) + + const content = await resp.text() + expect(content).toStrictEqual(await fs.readFile(__filename, "utf8")) + }) + }) + + describe("outside code-server root", () => { + it("should return a 401 for a nonexistent file", async () => { + const resp = await codeServer().fetch(`/static/${commit}/${nonExistentTestFile}`) + expect(resp.status).toBe(401) + + const content = await resp.json() + expect(content).toStrictEqual({ error: "Unauthorized" }) + }) + + it("should return a 401 for an existent file", async () => { + const resp = await codeServer().fetch(`/static/${commit}${testFile}`) + expect(resp.status).toBe(401) + + const content = await resp.json() + expect(content).toStrictEqual({ error: "Unauthorized" }) + }) + }) + }) +}) From ad4a70c684487502a253842933e68bb35f22f08e Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 5 May 2021 17:58:04 -0500 Subject: [PATCH 15/18] Use warn log level for integration tests Just to limit all the noise from code-server's startup output. --- test/utils/integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/integration.ts b/test/utils/integration.ts index 1ad7b44d..5c4f0cc6 100644 --- a/test/utils/integration.ts +++ b/test/utils/integration.ts @@ -3,7 +3,7 @@ import { runCodeServer } from "../../src/node/main" import * as httpserver from "./httpserver" export async function setup(argv: string[], configFile?: string): Promise { - argv = ["--bind-addr=localhost:0", ...argv] + argv = ["--bind-addr=localhost:0", "--log=warn", ...argv] const cliArgs = parse(argv) const configArgs = parseConfigFile(configFile || "", "test/integration.ts") From 14dbd16a7affd927b0abd3f398da112a48d0930c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 May 2021 01:30:47 +0530 Subject: [PATCH 16/18] chore(deps): bump lodash from 4.17.20 to 4.17.21 in /test (#3300) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/yarn.lock b/test/yarn.lock index d7e69732..8875e7c9 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -3462,9 +3462,9 @@ lodash.sortby@^4.7.0: integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== lru-cache@^6.0.0: version "6.0.0" From e8443e2602e4e0ed83e34565d15bff1754b1daf0 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 6 May 2021 11:25:29 -0500 Subject: [PATCH 17/18] Fix helpers not working in e2e tests It errors that jest is not defined so put it behind a function instead of immediately creating the mock (this is probably a better pattern anyway). The constant tests had to be reworked a little. Since the logger mock is hoisted it runs before createLoggerMock is imported. I moved it into a beforeAll which means the require call also needed to be moved there (since we need to mock the logger before requiring the constants or it'll pull the non-mocked logger). This means getPackageJson needs to be a let and assigned afterward. To avoid having to define a type for getPackageJson I just added a let var set to the type of the imported constants file and modified the other areas to use the same paradigm. I also replaced some hardcoded strings with the mocked package.json object. --- test/unit/constants.test.ts | 51 ++++++++++++++----------------------- test/unit/register.test.ts | 4 ++- test/unit/util.test.ts | 4 ++- test/utils/helpers.ts | 25 ++++++++++-------- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/test/unit/constants.test.ts b/test/unit/constants.test.ts index 88654457..fe9c26d2 100644 --- a/test/unit/constants.test.ts +++ b/test/unit/constants.test.ts @@ -1,29 +1,22 @@ -import { loggerModule } from "../utils/helpers" - -// jest.mock is hoisted above the imports so we must use `require` here. -jest.mock("@coder/logger", () => require("../utils/helpers").loggerModule) +import { createLoggerMock } from "../utils/helpers" describe("constants", () => { - beforeAll(() => { - jest.clearAllMocks() - jest.resetModules() - }) + let constants: typeof import("../../src/node/constants") + describe("with package.json defined", () => { - const { getPackageJson } = require("../../src/node/constants") - let mockPackageJson = { + const loggerModule = createLoggerMock() + const mockPackageJson = { name: "mock-code-server", description: "Run VS Code on a remote server.", repository: "https://github.com/cdr/code-server", version: "1.0.0", commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b", } - let version = "" - let commit = "" - beforeEach(() => { + beforeAll(() => { + jest.mock("@coder/logger", () => loggerModule) jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) - commit = require("../../src/node/constants").commit - version = require("../../src/node/constants").version + constants = require("../../src/node/constants") }) afterAll(() => { @@ -32,18 +25,18 @@ describe("constants", () => { }) it("should provide the commit", () => { - expect(commit).toBe("f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b") + expect(constants.commit).toBe(mockPackageJson.commit) }) it("should return the package.json version", () => { - expect(version).toBe(mockPackageJson.version) + expect(constants.version).toBe(mockPackageJson.version) }) describe("getPackageJson", () => { it("should log a warning if package.json not found", () => { const expectedErrorMessage = "Cannot find module './package.json' from 'src/node/constants.ts'" - getPackageJson("./package.json") + constants.getPackageJson("./package.json") expect(loggerModule.logger.warn).toHaveBeenCalled() expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage) @@ -52,38 +45,32 @@ describe("constants", () => { it("should find the package.json", () => { // the function calls require from src/node/constants // so to get the root package.json we need to use ../../ - const packageJson = getPackageJson("../../package.json") - expect(Object.keys(packageJson).length).toBeGreaterThan(0) - expect(packageJson.name).toBe("mock-code-server") - expect(packageJson.description).toBe("Run VS Code on a remote server.") - expect(packageJson.repository).toBe("https://github.com/cdr/code-server") + const packageJson = constants.getPackageJson("../../package.json") + expect(packageJson).toStrictEqual(mockPackageJson) }) }) }) describe("with incomplete package.json", () => { - let mockPackageJson = { + const mockPackageJson = { name: "mock-code-server", } - let version = "" - let commit = "" - beforeEach(() => { + beforeAll(() => { jest.mock("../../package.json", () => mockPackageJson, { virtual: true }) - version = require("../../src/node/constants").version - commit = require("../../src/node/constants").commit + constants = require("../../src/node/constants") }) - afterEach(() => { + afterAll(() => { jest.clearAllMocks() jest.resetModules() }) it("version should return 'development'", () => { - expect(version).toBe("development") + expect(constants.version).toBe("development") }) it("commit should return 'development'", () => { - expect(commit).toBe("development") + expect(constants.commit).toBe("development") }) }) }) diff --git a/test/unit/register.test.ts b/test/unit/register.test.ts index 4aa2006f..da69f21d 100644 --- a/test/unit/register.test.ts +++ b/test/unit/register.test.ts @@ -1,6 +1,6 @@ import { JSDOM } from "jsdom" import { registerServiceWorker } from "../../src/browser/register" -import { loggerModule } from "../utils/helpers" +import { createLoggerMock } from "../utils/helpers" import { LocationLike } from "./util.test" describe("register", () => { @@ -21,6 +21,7 @@ describe("register", () => { }) }) + const loggerModule = createLoggerMock() beforeEach(() => { jest.clearAllMocks() jest.mock("@coder/logger", () => loggerModule) @@ -75,6 +76,7 @@ describe("register", () => { }) describe("when navigator and serviceWorker are NOT defined", () => { + const loggerModule = createLoggerMock() beforeEach(() => { jest.clearAllMocks() jest.mock("@coder/logger", () => loggerModule) diff --git a/test/unit/util.test.ts b/test/unit/util.test.ts index 66ea0ce2..e63fcde5 100644 --- a/test/unit/util.test.ts +++ b/test/unit/util.test.ts @@ -11,7 +11,7 @@ import { trimSlashes, normalize, } from "../../src/common/util" -import { loggerModule } from "../utils/helpers" +import { createLoggerMock } from "../utils/helpers" const dom = new JSDOM() global.document = dom.window.document @@ -229,6 +229,8 @@ describe("util", () => { jest.restoreAllMocks() }) + const loggerModule = createLoggerMock() + it("should log an error with the message and stack trace", () => { const message = "You don't have access to that folder." const error = new Error(message) diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index df84ec1f..f31752b8 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -2,16 +2,21 @@ import * as fs from "fs" import * as os from "os" import * as path from "path" -export const loggerModule = { - field: jest.fn(), - level: 2, - logger: { - debug: jest.fn(), - error: jest.fn(), - info: jest.fn(), - trace: jest.fn(), - warn: jest.fn(), - }, +/** + * Return a mock of @coder/logger. + */ +export function createLoggerMock() { + return { + field: jest.fn(), + level: 2, + logger: { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + trace: jest.fn(), + warn: jest.fn(), + }, + } } /** From ae708dbed4cf35377c281a4174eb31118594022a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 May 2021 20:01:49 +0000 Subject: [PATCH 18/18] chore(deps): bump lodash from 4.17.20 to 4.17.21 in /lib/vscode Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) Signed-off-by: dependabot[bot] --- lib/vscode/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/vscode/yarn.lock b/lib/vscode/yarn.lock index c3c3779e..c27212ac 100644 --- a/lib/vscode/yarn.lock +++ b/lib/vscode/yarn.lock @@ -5634,9 +5634,9 @@ lodash.uniq@^4.5.0: integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@4.0.0: version "4.0.0"