Merge pull request #3286 from code-asher/permessage-deflate

This commit is contained in:
Asher 2021-05-05 14:32:06 -05:00 committed by GitHub
commit af5a1c9861
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 204 additions and 173 deletions

View File

@ -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<Required<Args>> = {
"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." },

View File

@ -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"

View File

@ -1,13 +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,
optionDescriptions,
parse,
readConfigFile,
@ -15,149 +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<void> => {
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<void> => {
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.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<void> {
proxyAgent.monkeyPatch(false)
@ -170,7 +24,8 @@ async function entry(): Promise<void> {
if (isChild(wrapper)) {
const args = await wrapper.handshake()
wrapper.preventExit()
return main(args)
await runCodeServer(args)
return
}
const cliArgs = parse(process.argv.slice(2))

159
src/node/main.ts Normal file
View File

@ -0,0 +1,159 @@
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"
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<void> => {
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<http.Server> => {
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) {
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
logger.info(" - Connected to cloud agent")
}
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))
}
}
return server
}

View File

@ -7,7 +7,8 @@ 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 { Feature } from "../cli"
import { isDevMode, rootPath, version } from "../constants"
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
import { getMediaMime, pathToFsPath } from "../util"
import { VscodeProvider } from "../vscode"
@ -31,7 +32,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 +45,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(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
!isDevMode ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
{
authed: req.args.auth !== "none",
disableTelemetry: !!req.args["disable-telemetry"],
@ -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)
})

View File

@ -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")],

View File

@ -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)

View File

@ -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"),

View File

@ -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<httpserver.HttpServer> {
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)
}