From dcb303a437be8c653eb78e67f314db3cb589a79a Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 16:17:04 -0500 Subject: [PATCH 01/82] Move argument defaults into setDefaults --- src/node/cli.ts | 135 +++++++++++++++++++++++++++++++++++----------- src/node/entry.ts | 91 +++++++++++-------------------- src/node/http.ts | 12 ++--- test/cli.test.ts | 73 ++++++++++++++++++++++++- 4 files changed, 209 insertions(+), 102 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 1403d8920..ba8f5ae88 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -5,7 +5,7 @@ import * as os from "os" import * as path from "path" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" import { AuthType } from "./http" -import { canConnect, generatePassword, humanPath, paths } from "./util" +import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" export class Optional { public constructor(public readonly value?: T) {} @@ -22,33 +22,33 @@ export enum LogLevel { export class OptionalString extends Optional {} export interface Args extends VsArgs { - readonly config?: string - readonly auth?: AuthType - readonly password?: string - readonly cert?: OptionalString - readonly "cert-key"?: string - readonly "disable-telemetry"?: boolean - readonly help?: boolean - readonly host?: string - readonly json?: boolean + config?: string + auth?: AuthType + password?: string + cert?: OptionalString + "cert-key"?: string + "disable-telemetry"?: boolean + help?: boolean + host?: string + json?: boolean log?: LogLevel - readonly open?: boolean - readonly port?: number - readonly "bind-addr"?: string - readonly socket?: string - readonly version?: boolean - readonly force?: boolean - readonly "list-extensions"?: boolean - readonly "install-extension"?: string[] - readonly "show-versions"?: boolean - readonly "uninstall-extension"?: string[] - readonly "proxy-domain"?: string[] - readonly locale?: string - readonly _: string[] - readonly "reuse-window"?: boolean - readonly "new-window"?: boolean + open?: boolean + port?: number + "bind-addr"?: string + socket?: string + version?: boolean + force?: boolean + "list-extensions"?: boolean + "install-extension"?: string[] + "show-versions"?: boolean + "uninstall-extension"?: string[] + "proxy-domain"?: string[] + locale?: string + _: string[] + "reuse-window"?: boolean + "new-window"?: boolean - readonly link?: OptionalString + link?: OptionalString } interface Option { @@ -325,13 +325,37 @@ export const parse = ( args._.push(arg) } + // If a cert was provided a key must also be provided. + if (args.cert && args.cert.value && !args["cert-key"]) { + throw new Error("--cert-key is missing") + } + logger.debug("parsed command line", field("args", args)) return args } -export async function setDefaults(args: Args): Promise { - args = { ...args } +export interface DefaultedArgs extends ConfigArgs { + auth: AuthType + cert?: { + value: string + } + host: string + port: number + "proxy-domain": string[] + verbose: boolean + usingEnvPassword: boolean + "extensions-dir": string + "user-data-dir": string +} + +/** + * Take CLI and config arguments (optional) and return a single set of arguments + * with the defaults set. Arguments from the CLI are prioritized over config + * arguments. + */ +export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise { + const args = Object.assign({}, configArgs || {}, cliArgs) if (!args["user-data-dir"]) { await copyOldMacOSDataDir() @@ -381,7 +405,52 @@ export async function setDefaults(args: Args): Promise { break } - return args + // Default to using a password. + if (!args.auth) { + args.auth = AuthType.Password + } + + const [host, port] = bindAddrFromAllSources(args, configArgs || { _: [] }) + args.host = host + args.port = port + + // If we're being exposed to the cloud, we listen on a random address and + // disable auth. + if (args.link) { + args.host = "localhost" + args.port = 0 + args.socket = undefined + args.cert = undefined + + if (args.auth !== AuthType.None) { + args.auth = AuthType.None + } + } + + if (args.cert && !args.cert.value) { + const { cert, certKey } = await generateCertificate() + args.cert = { + value: cert, + } + args["cert-key"] = certKey + } + + const usingEnvPassword = !!process.env.PASSWORD + if (process.env.PASSWORD) { + args.password = process.env.PASSWORD + } + + // Ensure it's not readable by child processes. + delete process.env.PASSWORD + + // Filter duplicate proxy domains and remove any leading `*.`. + const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, ""))) + args["proxy-domain"] = Array.from(proxyDomains) + + return { + ...args, + usingEnvPassword, + } as DefaultedArgs // TODO: Technically no guarantee this is fulfilled. } async function defaultConfigFile(): Promise { @@ -392,12 +461,16 @@ cert: false ` } +interface ConfigArgs extends Args { + config: string +} + /** * Reads the code-server yaml config file and returns it as Args. * * @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default. */ -export async function readConfigFile(configPath?: string): Promise { +export async function readConfigFile(configPath?: string): Promise { if (!configPath) { configPath = process.env.CODE_SERVER_CONFIG if (!configPath) { @@ -466,7 +539,7 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr { return addr } -export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { +function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { let addr: Addr = { host: "localhost", port: 8080, diff --git a/src/node/entry.ts b/src/node/entry.ts index 96db046e2..826d9a484 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -12,8 +12,7 @@ import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { - Args, - bindAddrFromAllSources, + DefaultedArgs, optionDescriptions, parse, readConfigFile, @@ -24,7 +23,7 @@ import { import { coderCloudBind } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" -import { generateCertificate, hash, humanPath, open } from "./util" +import { hash, humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" let pkg: { version?: string; commit?: string } = {} @@ -37,7 +36,7 @@ try { const version = pkg.version || "development" const commit = pkg.commit || "development" -export const runVsCodeCli = (args: Args): void => { +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: { @@ -61,7 +60,7 @@ export const runVsCodeCli = (args: Args): void => { vscode.on("exit", (code) => process.exit(code || 0)) } -export const openInExistingInstance = async (args: Args, socketPath: string): Promise => { +export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise => { const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { type: "open", folderURIs: [], @@ -117,54 +116,26 @@ export const openInExistingInstance = async (args: Args, socketPath: string): Pr vscode.end() } -const main = async (args: Args, configArgs: Args): Promise => { - if (args.link) { - // If we're being exposed to the cloud, we listen on a random address and disable auth. - args = { - ...args, - host: "localhost", - port: 0, - auth: AuthType.None, - socket: undefined, - cert: undefined, - } - logger.info("link: disabling auth and listening on random localhost port for cloud agent") - } - - if (!args.auth) { - args = { - ...args, - auth: AuthType.Password, - } - } - +const main = async (args: DefaultedArgs): Promise => { logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`) - logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`) - const envPassword = !!process.env.PASSWORD - const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password) - if (args.auth === AuthType.Password && !password) { + if (args.auth === AuthType.Password && !args.password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } - const [host, port] = bindAddrFromAllSources(args, configArgs) // Spawn the main HTTP server. const options: HttpServerOptions = { auth: args.auth, commit, - host: host, + host: args.host, // The hash does not add any actual security but we do it for obfuscation purposes. - password: password ? hash(password) : undefined, - port: port, + password: args.password ? hash(args.password) : undefined, + port: args.port, proxyDomains: args["proxy-domain"], socket: args.socket, - ...(args.cert && !args.cert.value - ? await generateCertificate() - : { - cert: args.cert && args.cert.value, - certKey: args["cert-key"], - }), + cert: args.cert && args.cert.value, + certKey: args["cert-key"], } if (options.cert && !options.certKey) { @@ -175,7 +146,7 @@ const main = async (args: Args, configArgs: Args): Promise => { httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args) httpServer.registerHttpProvider("/update", UpdateHttpProvider, false) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) - httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) + httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, args.usingEnvPassword) httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart) @@ -191,19 +162,18 @@ const main = async (args: Args, configArgs: Args): Promise => { logger.info(`Using config file ${humanPath(args.config)}`) const serverAddress = await httpServer.listen() - logger.info(`HTTP server listening on ${serverAddress}`) + logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) if (args.auth === AuthType.Password) { - if (envPassword) { + if (args.usingEnvPassword) { logger.info(" - Using password from $PASSWORD") } else { logger.info(` - Using password from ${humanPath(args.config)}`) } logger.info(" - To disable use `--auth none`") } else { - logger.info(" - No authentication") + logger.info(` - No authentication ${args.link ? "(disabled by --link)" : ""}`) } - delete process.env.PASSWORD if (httpServer.protocol === "https") { logger.info( @@ -215,9 +185,19 @@ const main = async (args: Args, configArgs: Args): Promise => { logger.info(" - Not serving HTTPS") } - if (httpServer.proxyDomains.size > 0) { - logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`) - httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) + 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!, args.link.value) + logger.info(" - Connected to cloud agent") + } catch (err) { + logger.error(err.message) + ipcMain.exit(1) + } } if (serverAddress && !options.socket && args.open) { @@ -228,23 +208,12 @@ const main = async (args: Args, configArgs: Args): Promise => { }) logger.info(`Opened ${openAddress}`) } - - if (args.link) { - try { - await coderCloudBind(serverAddress!, args.link.value) - } catch (err) { - logger.error(err.message) - ipcMain.exit(1) - } - } } async function entry(): Promise { const cliArgs = parse(process.argv.slice(2)) const configArgs = await readConfigFile(cliArgs.config) - // This prioritizes the flags set in args over the ones in the config file. - let args = Object.assign(configArgs, cliArgs) - args = await setDefaults(args) + const args = await setDefaults(cliArgs, configArgs) // There's no need to check flags like --help or to spawn in an existing // instance for the child process because these would have already happened in @@ -252,7 +221,7 @@ async function entry(): Promise { if (ipcMain.isChild) { await ipcMain.handshake() ipcMain.preventExit() - return main(args, configArgs) + return main(args) } if (args.help) { diff --git a/src/node/http.ts b/src/node/http.ts index c616c8837..93d6e725c 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -130,7 +130,7 @@ export interface HttpServerOptions { readonly host?: string readonly password?: string readonly port?: number - readonly proxyDomains?: string[] + readonly proxyDomains: string[] readonly socket?: string } @@ -463,18 +463,12 @@ export class HttpServer { public readonly heart: Heart private readonly socketProvider = new SocketProxyProvider() - /** - * Proxy domains are stored here without the leading `*.` - */ - public readonly proxyDomains: Set - /** * Provides the actual proxying functionality. */ private readonly proxy = proxy.createProxyServer({}) public constructor(private readonly options: HttpServerOptions) { - this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, ""))) this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { const connections = await this.getConnections() logger.trace(plural(connections, `${connections} active connection`)) @@ -892,7 +886,7 @@ export class HttpServer { return undefined } - this.proxyDomains.forEach((domain) => { + this.options.proxyDomains.forEach((domain) => { if (host.endsWith(domain) && domain.length < host.length) { host = domain } @@ -922,7 +916,7 @@ export class HttpServer { // There must be an exact match. const port = parts.shift() const proxyDomain = parts.join(".") - if (!port || !this.proxyDomains.has(proxyDomain)) { + if (!port || !this.options.proxyDomains.includes(proxyDomain)) { return undefined } diff --git a/test/cli.test.ts b/test/cli.test.ts index ae5256142..a59aac995 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -14,17 +14,23 @@ type Mutable = { describe("parser", () => { beforeEach(() => { delete process.env.LOG_LEVEL + delete process.env.PASSWORD }) // The parser should not set any defaults so the caller can determine what // values the user actually set. These are only set after explicitly calling // `setDefaults`. const defaults = { + auth: "password", + host: "localhost", + port: 8080, + "proxy-domain": [], + usingEnvPassword: false, "extensions-dir": path.join(paths.data, "extensions"), "user-data-dir": paths.data, } - it("should set defaults", () => { + it("should parse nothing", () => { assert.deepEqual(parse([]), { _: [] }) }) @@ -232,6 +238,71 @@ describe("parser", () => { "proxy-domain": ["*.coder.com", "test.com"], }) }) + + it("should enforce cert-key with cert value or otherwise generate one", async () => { + const args = parse(["--cert"]) + assert.deepEqual(args, { + _: [], + cert: { + value: undefined, + }, + }) + assert.throws(() => parse(["--cert", "test"]), /--cert-key is missing/) + assert.deepEqual(await setDefaults(args), { + _: [], + ...defaults, + cert: { + value: path.join(tmpdir, "self-signed.cert"), + }, + "cert-key": path.join(tmpdir, "self-signed.key"), + }) + }) + + it("should override with --link", async () => { + const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" ")) + assert.deepEqual(await setDefaults(args), { + _: [], + ...defaults, + auth: "none", + host: "localhost", + link: { + value: "test", + }, + port: 0, + cert: undefined, + "cert-key": path.resolve("test"), + socket: undefined, + }) + }) + + it("should use env var password", async () => { + process.env.PASSWORD = "test" + const args = parse([]) + assert.deepEqual(args, { + _: [], + }) + + assert.deepEqual(await setDefaults(args), { + ...defaults, + _: [], + password: "test", + usingEnvPassword: true, + }) + }) + + it("should filter proxy domains", async () => { + const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"]) + assert.deepEqual(args, { + _: [], + "proxy-domain": ["*.coder.com", "coder.com", "coder.org"], + }) + + assert.deepEqual(await setDefaults(args), { + ...defaults, + _: [], + "proxy-domain": ["coder.com", "coder.org"], + }) + }) }) describe("cli", () => { From 2928d362fa7e40ba2cbeecb57275a296b1903ca3 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 17:00:21 -0500 Subject: [PATCH 02/82] Move heart and AuthType out of http This file is going to get blasted in favor of Express. --- src/node/app/health.ts | 3 ++- src/node/app/login.ts | 3 ++- src/node/cli.ts | 6 ++++- src/node/heart.ts | 46 +++++++++++++++++++++++++++++++++++++ src/node/http.ts | 51 ++---------------------------------------- test/update.test.ts | 5 ++--- 6 files changed, 59 insertions(+), 55 deletions(-) create mode 100644 src/node/heart.ts diff --git a/src/node/app/health.ts b/src/node/app/health.ts index 48d6897cf..4e505f57e 100644 --- a/src/node/app/health.ts +++ b/src/node/app/health.ts @@ -1,4 +1,5 @@ -import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http" +import { Heart } from "../heart" +import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http" /** * Check the heartbeat. diff --git a/src/node/app/login.ts b/src/node/app/login.ts index 0fe1e0b6e..58bd55e0a 100644 --- a/src/node/app/login.ts +++ b/src/node/app/login.ts @@ -2,7 +2,8 @@ import * as http from "http" import * as limiter from "limiter" import * as querystring from "querystring" import { HttpCode, HttpError } from "../../common/http" -import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" +import { AuthType } from "../cli" +import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { hash, humanPath } from "../util" interface LoginPayload { diff --git a/src/node/cli.ts b/src/node/cli.ts index ba8f5ae88..7f44f9ea5 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -4,9 +4,13 @@ import yaml from "js-yaml" import * as os from "os" import * as path from "path" import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc" -import { AuthType } from "./http" import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util" +export enum AuthType { + Password = "password", + None = "none", +} + export class Optional { public constructor(public readonly value?: T) {} } diff --git a/src/node/heart.ts b/src/node/heart.ts new file mode 100644 index 000000000..5198e33d8 --- /dev/null +++ b/src/node/heart.ts @@ -0,0 +1,46 @@ +import { logger } from "@coder/logger" +import { promises as fs } from "fs" + +/** + * Provides a heartbeat using a local file to indicate activity. + */ +export class Heart { + private heartbeatTimer?: NodeJS.Timeout + private heartbeatInterval = 60000 + public lastHeartbeat = 0 + + public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} + + public alive(): boolean { + const now = Date.now() + return now - this.lastHeartbeat < this.heartbeatInterval + } + /** + * Write to the heartbeat file if we haven't already done so within the + * timeout and start or reset a timer that keeps running as long as there is + * activity. Failures are logged as warnings. + */ + public beat(): void { + if (!this.alive()) { + logger.trace("heartbeat") + fs.writeFile(this.heartbeatPath, "").catch((error) => { + logger.warn(error.message) + }) + this.lastHeartbeat = Date.now() + if (typeof this.heartbeatTimer !== "undefined") { + clearTimeout(this.heartbeatTimer) + } + this.heartbeatTimer = setTimeout(() => { + this.isActive() + .then((active) => { + if (active) { + this.beat() + } + }) + .catch((error) => { + logger.warn(error.message) + }) + }, this.heartbeatInterval) + } + } +} diff --git a/src/node/http.ts b/src/node/http.ts index 93d6e725c..4aa6dc067 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -13,6 +13,8 @@ import * as tls from "tls" import * as url from "url" import { HttpCode, HttpError } from "../common/http" import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util" +import { AuthType } from "./cli" +import { Heart } from "./heart" import { SocketProxyProvider } from "./socket" import { getMediaMime, paths } from "./util" @@ -27,11 +29,6 @@ interface AuthPayload extends Cookies { key?: string[] } -export enum AuthType { - Password = "password", - None = "none", -} - export type Query = { [key: string]: string | string[] | undefined } export interface ProxyOptions { @@ -390,50 +387,6 @@ export abstract class HttpProvider { } } -/** - * Provides a heartbeat using a local file to indicate activity. - */ -export class Heart { - private heartbeatTimer?: NodeJS.Timeout - private heartbeatInterval = 60000 - public lastHeartbeat = 0 - - public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} - - public alive(): boolean { - const now = Date.now() - return now - this.lastHeartbeat < this.heartbeatInterval - } - /** - * Write to the heartbeat file if we haven't already done so within the - * timeout and start or reset a timer that keeps running as long as there is - * activity. Failures are logged as warnings. - */ - public beat(): void { - if (!this.alive()) { - logger.trace("heartbeat") - fs.outputFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) - this.lastHeartbeat = Date.now() - if (typeof this.heartbeatTimer !== "undefined") { - clearTimeout(this.heartbeatTimer) - } - this.heartbeatTimer = setTimeout(() => { - this.isActive() - .then((active) => { - if (active) { - this.beat() - } - }) - .catch((error) => { - logger.warn(error.message) - }) - }, this.heartbeatInterval) - } - } -} - export interface HttpProvider0 { new (options: HttpProviderOptions): T } diff --git a/test/update.test.ts b/test/update.test.ts index 7e4b80f21..9e27eefaf 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -3,12 +3,11 @@ import * as fs from "fs-extra" import * as http from "http" import * as path from "path" import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update" -import { AuthType } from "../src/node/http" +import { AuthType } from "../src/node/cli" import { SettingsProvider, UpdateSettings } from "../src/node/settings" import { tmpdir } from "../src/node/util" -describe("update", () => { - return +describe.skip("update", () => { let version = "1.0.0" let spy: string[] = [] const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { From 6000e389bc6778f86f1ef1c18c2ff4d8362f27ec Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 17:05:55 -0500 Subject: [PATCH 03/82] Add Express as a dep and regenerate lockfile The Express types were throwing errors but regenerating the lockfile resolved them. --- package.json | 2 + yarn.lock | 1034 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 675 insertions(+), 361 deletions(-) diff --git a/package.json b/package.json index cc3edd30a..e6197065f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "main": "out/node/entry.js", "devDependencies": { + "@types/express": "^4.17.8", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", "@types/js-yaml": "^3.12.3", @@ -67,6 +68,7 @@ "dependencies": { "@coder/logger": "1.1.16", "env-paths": "^2.2.0", + "express": "^4.17.1", "fs-extra": "^9.0.1", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/yarn.lock b/yarn.lock index 6f388626b..d1224a32f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,28 +9,24 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" - integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== - dependencies: - browserslist "^4.12.0" - invariant "^2.2.4" - semver "^5.5.0" +"@babel/compat-data@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.0.tgz#443aea07a5aeba7942cb067de6b8272f2ab36b9e" + integrity sha512-jAbCtMANC9ptXxbSVXIqV/3H0bkh7iyyv6JS5lu10av45bcc2QmDNJXkASZCFwbBt75Q0AEq/BB+bNa3x1QgYQ== "@babel/core@>=7.9.0", "@babel/core@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.4.tgz#4301dfdfafa01eeb97f1896c5501a3f0655d4229" - integrity sha512-5deljj5HlqRXN+5oJTY7Zs37iH3z3b++KjiKtIsJy1NrjOOVSEaJHEetLBhyu0aQOSNNZ/0IuEAan9GzRuDXHg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.0.tgz#e42e07a086e978cdd4c61f4078d8230fb817cc86" + integrity sha512-iV7Gwg0DePKvdDZZWRTkj4MW+6/AbVWd4ZCg+zk8H1RVt5xBpUZS6vLQWwb3pyLg4BFTaGiQCPoJ4Ibmbne4fA== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.4" - "@babel/helper-module-transforms" "^7.11.0" + "@babel/generator" "^7.12.0" + "@babel/helper-module-transforms" "^7.12.0" "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.4" + "@babel/parser" "^7.12.0" "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,12 +36,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.11.0", "@babel/generator@^7.11.4", "@babel/generator@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.4.tgz#1ec7eec00defba5d6f83e50e3ee72ae2fee482be" - integrity sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g== +"@babel/generator@^7.12.0", "@babel/generator@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.0.tgz#91a45f1c18ca8d895a35a04da1a4cf7ea3f37f98" + integrity sha512-8lnf4QcyiQMf5XQp47BltuMTocsOh6P0z/vueEh8GzhmWWlDbdvOoI5Ziddg0XYhmnx35HyByUW51/9NprF8cA== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" jsesc "^2.5.1" source-map "^0.5.0" @@ -65,13 +61,13 @@ "@babel/types" "^7.10.4" "@babel/helper-builder-react-jsx-experimental@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" - integrity sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.0.tgz#e8655888d0d36fd2a15c02decf77923fc18e95cd" + integrity sha512-AFzu6ib4i56olCtulkbIifcTay0O5tv8ZVK8hZMzrpu+YjsIDEcesF1DMqqTzV65clu3X61aE7qeHcJsY/gmnA== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-module-imports" "^7.10.4" - "@babel/types" "^7.10.5" + "@babel/types" "^7.12.0" "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" @@ -81,37 +77,36 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" - integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== +"@babel/helper-compilation-targets@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.0.tgz#c477d89a1f4d626c8149b9b88802f78d66d0c99a" + integrity sha512-NbDFJNjDgxE7IkrHp5gq2+Tr8bEdCLKYN90YDQEjMiTMUAFAcShNkaH8kydcmU0mEQTiQY0Ydy/+1xfS2OCEnw== dependencies: - "@babel/compat-data" "^7.10.4" + "@babel/compat-data" "^7.12.0" + "@babel/helper-validator-option" "^7.12.0" browserslist "^4.12.0" - invariant "^2.2.4" - levenary "^1.1.1" semver "^5.5.0" "@babel/helper-create-class-features-plugin@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" - integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.0.tgz#f3f2fc77bacc89e59ce6764daeabc1fb23e79a05" + integrity sha512-9tD1r9RK928vxvxcoNK8/7uwT7Q2DJZP1dnJmyMAJPwOF0yr8PPwqdpyw33lUpCfrJ765bOs5XNa4KSfUDWFSw== dependencies: "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-member-expression-to-functions" "^7.12.0" "@babel/helper-optimise-call-expression" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.0" "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-regexp-features-plugin@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" - integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.0.tgz#858cef57039f3b3a9012273597288a71e1dff8ca" + integrity sha512-YBqH+3wLcom+tko8/JLgRcG8DMqORgmjqNRNI751gTioJSZHWFybO1mRoLtJtWIlYSHY+zT9LqqnbbK1c3KIVQ== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-regex" "^7.10.4" - regexpu-core "^4.7.0" + regexpu-core "^4.7.1" "@babel/helper-define-map@^7.10.4": version "7.10.5" @@ -152,12 +147,12 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== +"@babel/helper-member-expression-to-functions@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.0.tgz#48f605fa801764f3e5b2e301e49d35fe1820c4f3" + integrity sha512-I0d/bgzgzgLsJMk7UZ0TN2KV3OGjC/t/9Saz8PKb9jrcEAXhgjGysOgp4PDKydIKjUv/gj2St4ae+ov8l+T9Xg== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" "@babel/helper-module-imports@^7.10.4": version "7.10.4" @@ -166,17 +161,19 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.0.tgz#8ac7d9e8716f94549a42e577c5429391950e33f3" + integrity sha512-1ZTMoCiLSzTJLbq7mSaTHki4oIrBIf/dUbzdhwTrvtMU3ZNVKwQmGae3gSiqppo7G8HAgnXmc43rfEaD8yYLLQ== dependencies: "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.0" "@babel/helper-simple-access" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.10.4": @@ -208,15 +205,15 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== +"@babel/helper-replace-supers@^7.10.4", "@babel/helper-replace-supers@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.0.tgz#98d3f3eb779752e59c7422ab387c9b444323be60" + integrity sha512-9kycFdq2c9e7PXZOr2z/ZqTFF9OzFu287iFwYS+CiDVPuoTCfY8hoTsIqNQNetQjlqoRsRyJFrMG1uhGAR4EEw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.0" "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/traverse" "^7.12.0" + "@babel/types" "^7.12.0" "@babel/helper-simple-access@^7.10.4": version "7.10.4" @@ -245,6 +242,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-option@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.0.tgz#1d1fc48a9b69763da61b892774b0df89aee1c969" + integrity sha512-NRfKaAQw/JCMsTFUdJI6cp4MoJGGVBRQTRSiW1nwlGldNqzjB9jqWI0SZqQksC724dJoKqwG+QqfS9ib7SoVsw== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -273,10 +275,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.4", "@babel/parser@^7.4.4": - version "7.11.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.4.tgz#6fa1a118b8b0d80d0267b719213dc947e88cc0ca" - integrity sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA== +"@babel/parser@^7.10.4", "@babel/parser@^7.12.0", "@babel/parser@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.0.tgz#2ad388f3960045b22f9b7d4bf85e80b15a1c9e3a" + integrity sha512-dYmySMYnlus2jwl7JnnajAj11obRStZoW9cG04wh4ZuhozDn11tDUrhHcUZ9iuNHqALAhh60XqNaYXpvuuE/Gg== "@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" @@ -303,10 +305,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-export-namespace-from@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" - integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== +"@babel/plugin-proposal-export-namespace-from@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.0.tgz#08b0f8100bbae1199a5f5294f38a1b0b8d8402fc" + integrity sha512-ao43U2ptSe+mIZAQo2nBV5Wx2Ie3i2XbLt8jCXZpv+bvLY1Twv0lak4YZ1Ps5OwbeLMAl3iOVScgGMOImBae1g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" @@ -319,26 +321,26 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" - integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== +"@babel/plugin-proposal-logical-assignment-operators@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.0.tgz#830d8ff4984d800b2824e8eac0005ecb7430328e" + integrity sha512-dssjXHzdMQal4q6GCSwDTVPEbyBLdd9+7aSlzAkQbrGEKq5xG8pvhQ7u2ktUrCLRmzQphZnSzILBL5ta4xSRlA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" - integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.0.tgz#d82174a531305df4d7079ce3782269b35b810b82" + integrity sha512-JpNWix2VP2ue31r72fKytTE13nPX1fxl1mudfTaTwcDhl3iExz5NZjQBq012b/BQ6URWoc/onI73pZdYlAfihg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" - integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== +"@babel/plugin-proposal-numeric-separator@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.0.tgz#76de244152abaf2e72800ab0aebc9771f6de3e9a" + integrity sha512-iON65YmIy/IpEgteYJ4HfO2q30SLdIxiyjNNlsSjSl0tUxLhSH9PljE5r6sczwdW64ZZzznYNcezdcROB+rDDw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" @@ -360,10 +362,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" - integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== +"@babel/plugin-proposal-optional-chaining@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.0.tgz#0159b549f165016fc9f284b8607a58a37a3b71fe" + integrity sha512-CXu9aw32FH/MksqdKvhpiH8pSvxnXJ33E7I7BGNE9VzNRpWgpNzvPpds/tW9E0pjmX9+D1zAHRyHbtyeTboo2g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" @@ -620,14 +622,15 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" - integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== +"@babel/plugin-transform-modules-systemjs@^7.12.0": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.0.tgz#bca842db6980cfc98ae7d0f2c907c9b1df3f874e" + integrity sha512-h2fDMnwRwBiNMmTGAWqUo404Z3oLbrPE6hyATecyIbsEsrbM5gjLbfKQLb6hjiouMlGHH+yliYBbc4NPgWKE/g== dependencies: "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-module-transforms" "^7.12.0" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-identifier" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.10.4": @@ -753,25 +756,26 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/preset-env@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" - integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.0.tgz#7d2d0c4f4a14ca0fd7d905a741070ab4745177b7" + integrity sha512-jSIHvHSuF+hBUIrvA2/61yIzhH+ceLOXGLTH1nwPvQlso/lNxXsoE/nvrCzY5M77KRzhKegB1CvdhWPZmYDZ5A== dependencies: - "@babel/compat-data" "^7.11.0" - "@babel/helper-compilation-targets" "^7.10.4" + "@babel/compat-data" "^7.12.0" + "@babel/helper-compilation-targets" "^7.12.0" "@babel/helper-module-imports" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.0" "@babel/plugin-proposal-async-generator-functions" "^7.10.4" "@babel/plugin-proposal-class-properties" "^7.10.4" "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.12.0" "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.0" + "@babel/plugin-proposal-numeric-separator" "^7.12.0" "@babel/plugin-proposal-object-rest-spread" "^7.11.0" "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-optional-chaining" "^7.12.0" "@babel/plugin-proposal-private-methods" "^7.10.4" "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" @@ -802,7 +806,7 @@ "@babel/plugin-transform-member-expression-literals" "^7.10.4" "@babel/plugin-transform-modules-amd" "^7.10.4" "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.12.0" "@babel/plugin-transform-modules-umd" "^7.10.4" "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" "@babel/plugin-transform-new-target" "^7.10.4" @@ -819,17 +823,15 @@ "@babel/plugin-transform-unicode-escapes" "^7.10.4" "@babel/plugin-transform-unicode-regex" "^7.10.4" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.0" browserslist "^4.12.0" core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" semver "^5.5.0" "@babel/preset-modules@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72" - integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -838,9 +840,9 @@ esutils "^2.0.2" "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.4": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.0.tgz#98bd7666186969c04be893d747cf4a6c6c8fa6b0" + integrity sha512-lS4QLXQ2Vbw2ubfQjeQcn+BZgZ5+ROHW9f+DWjEp5Y+NHYmkRGKqHSJ1tuhbUauKu2nhZNTBIvsIQ8dXfY5Gjw== dependencies: regenerator-runtime "^0.13.4" @@ -853,25 +855,25 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" - integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== +"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.0", "@babel/traverse@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.0.tgz#ed31953d6e708cdd34443de2fcdb55f72cdfb266" + integrity sha512-ZU9e79xpOukCNPkQ1UzR4gJKCruGckr6edd8v8lmKpSk8iakgUIvb+5ZtaKKV9f7O+x5r+xbMDDIbzVpUoiIuw== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" + "@babel/generator" "^7.12.0" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.4.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" - integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.0", "@babel/types@^7.4.4": + version "7.12.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.0.tgz#b6b49f425ee59043fbc89c61b11a13d5eae7b5c6" + integrity sha512-ggIyFmT2zMaYRheOfPDQ4gz7QqV3B+t2rjqjbttDJxMcb7/LukvWCmlIl1sWcOxrvwpTDd+z0OytzqsbGeb3/g== dependencies: "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.19" @@ -882,6 +884,22 @@ resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.16.tgz#ee5b1b188f680733f35c11b065bbd139d618c1e1" integrity sha512-X6VB1++IkosYY6amRAiMvuvCf12NA4+ooX+gOuu5bJIkdjmh4Lz7QpJcWRdgxesvo1msriDDr9E/sDbIWf6vsQ== +"@eslint/eslintrc@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" + integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@iarna/toml@^2.2.0": version "2.2.5" resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" @@ -962,7 +980,7 @@ "@parcel/utils" "^1.11.0" physical-cpu-count "^2.0.0" -"@stylelint/postcss-css-in-js@^0.37.1": +"@stylelint/postcss-css-in-js@^0.37.2": version "0.37.2" resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA== @@ -995,10 +1013,20 @@ traverse "^0.6.6" unified "^6.1.6" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" @@ -1006,14 +1034,24 @@ integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== "@types/express-serve-static-core@*": - version "4.17.9" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1" - integrity sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA== + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/fs-extra@^8.0.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068" @@ -1034,15 +1072,20 @@ integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== "@types/json-schema@^7.0.3": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + "@types/minimist@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" @@ -1054,9 +1097,9 @@ integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg== "@types/node@*", "@types/node@^12.12.7": - version "12.12.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.54.tgz#a4b58d8df3a4677b6c08bfbc94b7ad7a7a5f82d1" - integrity sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w== + version "12.12.67" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789" + integrity sha512-R48tgL2izApf+9rYNH+3RBMbRpPeW3N8f0I9HMhggeq4UXwBDqumJ14SDs4ctTMhG11pIOduZ4z3QWGOiMc9Vg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1088,9 +1131,9 @@ integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== "@types/qs@*": - version "6.9.4" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" - integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== "@types/range-parser@*": version "1.2.3" @@ -1103,9 +1146,17 @@ integrity sha512-1ri+LJhh0gRxIa37IpGytdaW7yDEHeJniBSMD1BmitS07R1j63brcYCzry+l0WJvGdEKQNQ7DYXO2epgborWPw== "@types/semver@^7.1.0": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" - integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== + version "7.3.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" + integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== + +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" "@types/split2@^2.1.6": version "2.1.6" @@ -1135,9 +1186,9 @@ integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== "@types/ws@^7.2.6": - version "7.2.6" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d" - integrity sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ== + version "7.2.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.7.tgz#362ad1a1d62721bdb725e72c8cccf357078cf5a3" + integrity sha512-UUFC/xxqFLP17hTva8/lVT0SybLUrfSD9c+iapKb0fEiC8uoDbA+xuZ3pAN603eW+bY8ebSMLm9jXdIPnD0ZgA== dependencies: "@types/node" "*" @@ -1202,9 +1253,17 @@ eslint-visitor-keys "^1.1.0" abab@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.4.tgz#6dfa57b417ca06d21b2478f0e638302f99c2405c" - integrity sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" acorn-globals@^4.3.0: version "4.3.4" @@ -1215,9 +1274,9 @@ acorn-globals@^4.3.0: acorn-walk "^6.0.1" acorn-jsx@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== acorn-walk@^6.0.1: version "6.2.0" @@ -1225,19 +1284,19 @@ acorn-walk@^6.0.1: integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== acorn@^6.0.1, acorn@^6.0.4: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: - version "6.12.4" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1293,12 +1352,11 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" ansi-to-html@^0.6.4: @@ -1356,6 +1414,11 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" @@ -1438,6 +1501,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -1463,7 +1531,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.8.0: +autoprefixer@^9.8.6: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== @@ -1572,7 +1640,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.0.1: +bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== @@ -1591,6 +1659,22 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -1719,14 +1803,14 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^4.0.0, browserslist@^4.1.0, browserslist@^4.12.0, browserslist@^4.8.5: - version "4.14.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.0.tgz#2908951abfe4ec98737b72f34c3bcedc8d43b000" - integrity sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ== + version "4.14.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== dependencies: - caniuse-lite "^1.0.30001111" - electron-to-chromium "^1.3.523" - escalade "^3.0.2" - node-releases "^1.1.60" + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1783,6 +1867,11 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1851,10 +1940,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111: - version "1.0.30001118" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz#116a9a670e5264aec895207f5e918129174c6f62" - integrity sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: + version "1.0.30001148" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001148.tgz#dc97c7ed918ab33bf8706ddd5e387287e015d637" + integrity sha512-E66qcd0KMKZHNJQt9hiLZGE3J4zuTqE1OnU53miEVtylFbwOEmeA5OsRu90noZful+XGSQOni1aT2tiqu/9yYw== caseless@~0.12.0: version "0.12.0" @@ -2065,21 +2154,21 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" color@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== dependencies: color-convert "^1.9.1" - color-string "^1.5.2" + color-string "^1.5.4" colorette@^1.2.1: version "1.2.1" @@ -2143,6 +2232,18 @@ contains-path@^0.1.0: resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -2150,6 +2251,16 @@ convert-source-map@^1.5.1, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -2183,16 +2294,16 @@ cosmiconfig@^5.0.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cosmiconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" + integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== dependencies: "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" + import-fresh "^3.2.1" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.7.2" + yaml "^1.10.0" create-ecdh@^4.0.0: version "4.0.4" @@ -2332,9 +2443,9 @@ css-tree@1.0.0-alpha.39: source-map "^0.6.1" css-what@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" - integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== cssesc@^3.0.0: version "3.0.0" @@ -2459,13 +2570,20 @@ debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -2615,9 +2733,9 @@ domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + version "2.0.2" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" + integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== domexception@^1.0.1: version "1.0.1" @@ -2642,9 +2760,9 @@ domutils@^1.5.1, domutils@^1.7.0: domelementtype "1" dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: is-obj "^2.0.0" @@ -2678,10 +2796,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.523: - version "1.3.549" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.549.tgz#bf500c8eb75a7286a895e34f41aa144384ac613b" - integrity sha512-q09qZdginlqDH3+Y1P6ch5UDTW8nZ1ijwMkxFs15J/DAWOwqolIx8HZH1UP0vReByBigk/dPlU22xS1MaZ+kpQ== +electron-to-chromium@^1.3.571: + version "1.3.581" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.581.tgz#7f796fe92cdc18f5013769dc6f45f4536315a183" + integrity sha512-ALORbI23YkYJoVJWusSdmTq8vXH3TLFzniILE47uZkZOim135ZhoTCM7QlIuvmK78As5kLdANfy7kDIQvJ+iPw== elliptic@^6.5.3: version "6.5.3" @@ -2758,19 +2876,37 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" object-keys "^1.1.1" - object.assign "^4.1.0" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" string.prototype.trimend "^1.0.1" string.prototype.trimstart "^1.0.1" @@ -2806,10 +2942,10 @@ es6-promisify@^6.0.0: resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.1.1.tgz#46837651b7b06bf6fff893d03f29393668d01621" integrity sha512-HBL8I3mIki5C1Cc9QjKUenHtnG0A5/xA8Q/AllRcfiwl2CZFXGK7ddBiCoRwAix4i2KxcQfjtIVcrVbB3vbmwg== -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== +escalade@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-html@~1.0.3: version "1.0.3" @@ -2851,13 +2987,13 @@ escodegen@~1.9.0: source-map "~0.6.1" eslint-config-prettier@^6.0.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" - integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== + version "6.12.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.12.0.tgz#9eb2bccff727db1c52104f0b49e87ea46605a0d2" + integrity sha512-9jWPlFlgNwRUYVoujvWTQ1aMO8o6648r+K7qU7K5Jmkbyqav1fuEZC0COYpGBxyiAJb65Ra9hrmFx19xRGwXWw== dependencies: get-stdin "^6.0.0" -eslint-import-resolver-node@^0.3.3: +eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== @@ -2874,16 +3010,16 @@ eslint-module-utils@^2.6.0: pkg-dir "^2.0.0" eslint-plugin-import@^2.18.2: - version "2.22.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" - integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== dependencies: array-includes "^3.1.1" array.prototype.flat "^1.2.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.3" + eslint-import-resolver-node "^0.3.4" eslint-module-utils "^2.6.0" has "^1.0.3" minimatch "^3.0.4" @@ -2899,12 +3035,12 @@ eslint-plugin-prettier@^3.1.0: dependencies: prettier-linter-helpers "^1.0.0" -eslint-scope@^5.0.0, eslint-scope@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" eslint-utils@^2.0.0, eslint-utils@^2.1.0: @@ -2919,22 +3055,28 @@ eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + eslint@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.7.0.tgz#18beba51411927c4b64da0a8ceadefe4030d6073" - integrity sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg== + version "7.11.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.11.0.tgz#aaf2d23a0b5f1d652a08edacea0c19f7fadc0b3b" + integrity sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== dependencies: "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.1.3" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" enquirer "^2.3.5" - eslint-scope "^5.1.0" + eslint-scope "^5.1.1" eslint-utils "^2.1.0" - eslint-visitor-keys "^1.3.0" - espree "^7.2.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.0" esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" @@ -2961,7 +3103,7 @@ eslint@^7.7.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.2.0: +espree@^7.3.0: version "7.3.0" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== @@ -2987,19 +3129,19 @@ esquery@^1.2.0: dependencies: estraverse "^5.1.0" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.1.0: +estraverse@^5.1.0, estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== @@ -3015,9 +3157,9 @@ etag@~1.8.1: integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= eventemitter3@^4.0.0: - version "4.0.6" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.6.tgz#1258f6fa51b4908aadc2cd624fcd6e64f99f49d6" - integrity sha512-s3GJL04SQoM+gn2c14oyqxvZ3Pcq7cduSDqy3sBFXx6UPSUmgVYwQM9zwkTn9je0lrfg0gHEwR42pF3Q2dCQkQ== + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^3.0.0: version "3.2.0" @@ -3052,6 +3194,42 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -3128,7 +3306,7 @@ fast-glob@^2.2.2: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.1.1: +fast-glob@^3.1.1, fast-glob@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== @@ -3150,6 +3328,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + fastparse@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -3203,6 +3386,19 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + find-up@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -3243,9 +3439,9 @@ flat-cache@^2.0.1: write "1.0.3" flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + version "4.1.1" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.1.tgz#a392059cc382881ff98642f5da4dde0a959f309b" + integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== dependencies: is-buffer "~2.0.3" @@ -3288,6 +3484,11 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -3683,6 +3884,17 @@ htmlparser2@~3.9.2: inherits "^2.0.1" readable-stream "^2.0.2" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -3757,7 +3969,7 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-fresh@^3.0.0, import-fresh@^3.1.0: +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -3813,12 +4025,10 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-absolute-url@^2.0.0: version "2.1.0" @@ -3901,10 +4111,10 @@ is-buffer@^2.0.0, is-buffer@~2.0.3: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== is-color-stop@^1.0.0: version "1.1.0" @@ -4023,6 +4233,11 @@ is-map@^2.0.1: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -4057,7 +4272,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.1.0: +is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== @@ -4173,7 +4388,7 @@ iterate-value@^1.0.0: es-get-iterator "^1.0.2" iterate-iterator "^1.0.1" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -4239,9 +4454,9 @@ json-parse-better-errors@^1.0.1: integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== json-parse-even-better-errors@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.0.tgz#371873c5ffa44304a6ba12419bcfa95f404ae081" - integrity sha512-o3aP+RsWDJZayj1SbHNQAI8x0v3T3SKiGoZlNYfbUP1S3omJQ6i9CnqADqkSPaOAxwua4/1YWx5CM7oiChJt2Q== + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^0.4.1: version "0.4.1" @@ -4334,18 +4549,6 @@ leaked-handles@^5.2.0: weakmap-shim "^1.1.0" xtend "^4.0.0" -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4432,7 +4635,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -4456,13 +4659,6 @@ longest-streak@^2.0.1: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - magic-string@^0.22.4: version "0.22.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" @@ -4549,10 +4745,15 @@ mdn-data@2.0.6: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== -meow@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.0.tgz#50ecbcdafa16f8b58fb7eb9675b933f6473b3a59" - integrity sha512-kq5F0KVteskZ3JdfyQFivJEj2RaA8NFsS4+r9DaMKLcUHpk5OcHS3Q0XkCXONB1mZRPsu/Y/qImKri0nwSEZog== +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +meow@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" + integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== dependencies: "@types/minimist" "^1.2.0" camelcase-keys "^6.2.2" @@ -4566,6 +4767,11 @@ meow@^7.0.1: type-fest "^0.13.1" yargs-parser "^18.1.3" +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + merge-source-map@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f" @@ -4578,6 +4784,11 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -4618,7 +4829,7 @@ mime-db@1.44.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.19: +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -4712,9 +4923,9 @@ mkdirp@^1.0.3: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mocha@^8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.2.tgz#d67fad13300e4f5cd48135a935ea566f96caf827" - integrity sha512-I8FRAcuACNMLQn3lS4qeWLxXqLvGf6r2CaLstDpZmMUUSmvW6Cnm1AuHxgbc7ctZVRcfwspCRbDHymPsi3dkJw== + version "8.1.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" + integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== dependencies: ansi-colors "4.1.1" browser-stdout "1.3.1" @@ -4758,9 +4969,9 @@ ms@2.1.2, ms@^2.1.1: integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + version "2.14.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" + integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== nanomatch@^1.2.9: version "1.2.13" @@ -4784,6 +4995,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4828,10 +5044,10 @@ node-libs-browser@^2.0.0: util "^0.11.0" vm-browserify "^1.0.1" -node-releases@^1.1.60: - version "1.1.60" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" - integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.61: + version "1.1.63" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.63.tgz#db6dbb388544c31e888216304e8fd170efee3ff5" + integrity sha512-ukW3iCfQaoxJkSPN+iK7KznTeqDGVJatAEuXsJERYHa9tn/KaT5lBdIyxQjLEVTzSkyjJEuQ17/vaEjrOauDkg== normalize-html-whitespace@^1.0.0: version "1.0.0" @@ -4911,7 +5127,7 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.7.0: +object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== @@ -4933,7 +5149,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@4.1.0, object.assign@^4.1.0: +object.assign@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -4943,6 +5159,16 @@ object.assign@4.1.0, object.assign@^4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + object.getownpropertydescriptors@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" @@ -5298,6 +5524,11 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -5369,9 +5600,9 @@ posix-character-classes@^0.1.0: integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= postcss-calc@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.3.tgz#d65cca92a3c52bf27ad37a5f732e0587b74f1623" - integrity sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA== + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== dependencies: postcss "^7.0.27" postcss-selector-parser "^6.0.2" @@ -5646,16 +5877,6 @@ postcss-reduce-transforms@^4.0.2: postcss "^7.0.0" postcss-value-parser "^3.0.0" -postcss-reporter@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-reporter/-/postcss-reporter-6.0.1.tgz#7c055120060a97c8837b4e48215661aafb74245f" - integrity sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw== - dependencies: - chalk "^2.4.1" - lodash "^4.17.11" - log-symbols "^2.2.0" - postcss "^7.0.7" - postcss-resolve-nested-selector@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" @@ -5683,7 +5904,7 @@ postcss-scss@^2.1.1: dependencies: postcss "^7.0.6" -postcss-selector-parser@6.0.2, postcss-selector-parser@^6.0.2: +postcss-selector-parser@6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== @@ -5701,6 +5922,16 @@ postcss-selector-parser@^3.0.0: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + postcss-svgo@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" @@ -5744,7 +5975,7 @@ postcss@6.0.1: source-map "^0.5.6" supports-color "^3.2.3" -postcss@7.0.32, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.6, postcss@^7.0.7: +postcss@7.0.32: version "7.0.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -5762,6 +5993,15 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + posthtml-parser@^0.4.0, posthtml-parser@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.4.2.tgz#a132bbdf0cd4bc199d34f322f5c1599385d7c6c1" @@ -5790,9 +6030,9 @@ posthtml@^0.11.2: posthtml-render "^1.1.5" posthtml@^0.13.1: - version "0.13.3" - resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.13.3.tgz#9702d745108d532a9d5808985e0dafd81b09f7bd" - integrity sha512-5NL2bBc4ihAyoYnY0EAQrFQbJNE1UdvgC1wjYts0hph7jYeU2fa5ki3/9U45ce9V6M1vLMEgUX2NXe/bYL+bCQ== + version "0.13.4" + resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.13.4.tgz#ad81b3fa62b85f81ccdb5710f4ec375a4ed94934" + integrity sha512-i2oTo/+dwXGC6zaAQSF6WZEQSbEqu10hsvg01DWzGAfZmy31Iiy9ktPh9nnXDfZiYytjxTIvxoK4TI0uk4QWpw== dependencies: posthtml-parser "^0.5.0" posthtml-render "^1.2.3" @@ -5815,9 +6055,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.0.5: - version "2.1.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" - integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== process-nextick-args@~2.0.0: version "2.0.1" @@ -5850,6 +6090,14 @@ promise.allsettled@1.0.2: function-bind "^1.1.1" iterate-value "^1.0.0" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -5905,6 +6153,11 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -5954,6 +6207,16 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -6078,10 +6341,10 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== -regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== +regexpu-core@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^8.2.0" @@ -6346,7 +6609,7 @@ run-parallel@^1.1.9: resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.2.0: +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -6428,7 +6691,7 @@ serialize-to-js@^3.0.0: resolved "https://registry.yarnpkg.com/serialize-to-js/-/serialize-to-js-3.1.1.tgz#b3e77d0568ee4a60bfe66287f991e104d3a1a4ac" integrity sha512-F+NGU0UHMBO4Q965tjw7rvieNVjlH6Lqi2emq/Lc9LUURYJbiCzmpi4Cy1OOjjVPtxu0c+NE85LU6968Wko5ZA== -serve-static@^1.12.4: +serve-static@1.14.1, serve-static@^1.12.4: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== @@ -6526,6 +6789,15 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -6612,9 +6884,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== specificity@^0.4.1: version "0.4.1" @@ -6841,7 +7113,7 @@ strip-json-comments@3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@^3.1.0: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -6873,18 +7145,20 @@ stylelint-config-recommended@^3.0.0: integrity sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ== stylelint@^13.0.0: - version "13.6.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.6.1.tgz#cc1d76338116d55e8ff2be94c4a4386c1239b878" - integrity sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw== + version "13.7.2" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-13.7.2.tgz#6f3c58eea4077680ed0ceb0d064b22b100970486" + integrity sha512-mmieorkfmO+ZA6CNDu1ic9qpt4tFvH2QUB7vqXgrMVHe5ENU69q7YDq0YUg/UHLuCsZOWhUAvcMcLzLDIERzSg== dependencies: - "@stylelint/postcss-css-in-js" "^0.37.1" + "@stylelint/postcss-css-in-js" "^0.37.2" "@stylelint/postcss-markdown" "^0.36.1" - autoprefixer "^9.8.0" + autoprefixer "^9.8.6" balanced-match "^1.0.0" chalk "^4.1.0" - cosmiconfig "^6.0.0" + cosmiconfig "^7.0.0" debug "^4.1.1" execall "^2.0.0" + fast-glob "^3.2.4" + fastest-levenshtein "^1.0.12" file-entry-cache "^5.0.1" get-stdin "^8.0.0" global-modules "^2.0.0" @@ -6895,18 +7169,16 @@ stylelint@^13.0.0: import-lazy "^4.0.0" imurmurhash "^0.1.4" known-css-properties "^0.19.0" - leven "^3.1.0" - lodash "^4.17.15" + lodash "^4.17.20" log-symbols "^4.0.0" mathml-tag-names "^2.1.3" - meow "^7.0.1" + meow "^7.1.1" micromatch "^4.0.2" normalize-selector "^0.2.0" postcss "^7.0.32" postcss-html "^0.36.0" postcss-less "^3.1.4" postcss-media-query-parser "^0.2.3" - postcss-reporter "^6.0.1" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^4.0.2" postcss-sass "^0.4.4" @@ -6922,7 +7194,7 @@ stylelint@^13.0.0: style-search "^0.1.0" sugarss "^2.0.0" svg-tags "^1.0.0" - table "^5.4.6" + table "^6.0.1" v8-compile-cache "^2.1.1" write-file-atomic "^3.0.3" @@ -6933,7 +7205,7 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" -supports-color@7.1.0, supports-color@^7.1.0: +supports-color@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== @@ -6966,6 +7238,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + svg-tags@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" @@ -6995,7 +7274,7 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3, table@^5.4.6: +table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== @@ -7005,6 +7284,16 @@ table@^5.2.3, table@^5.4.6: slice-ansi "^2.1.0" string-width "^3.0.0" +table@^6.0.1: + version "6.0.3" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.3.tgz#e5b8a834e37e27ad06de2e0fda42b55cfd8a0123" + integrity sha512-8321ZMcf1B9HvVX/btKv8mMZahCjn2aYrDlpqHaBFCfnox64edeH9kEid0vTLTRR8gWR2A20aDgeuTTea4sVtw== + dependencies: + ajv "^6.12.4" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" + tar-fs@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5" @@ -7016,11 +7305,11 @@ tar-fs@^2.0.0: tar-stream "^2.0.0" tar-stream@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41" - integrity sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA== + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== dependencies: - bl "^4.0.1" + bl "^4.0.3" end-of-stream "^1.4.1" fs-constants "^1.0.0" inherits "^2.0.3" @@ -7200,9 +7489,9 @@ tsconfig-paths@^3.9.0: strip-bom "^3.0.0" tslib@^1.8.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tsutils@^3.17.1: version "3.17.1" @@ -7257,6 +7546,14 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" @@ -7456,6 +7753,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -7480,9 +7782,9 @@ update-section@^0.3.0: integrity sha1-RY8Xgg03gg3GDiC4bZQ5GwASMVg= uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== dependencies: punycode "^2.1.0" @@ -7504,7 +7806,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -7533,6 +7835,11 @@ util@^0.11.0: dependencies: inherits "2.0.3" +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -7551,6 +7858,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -7789,7 +8101,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== @@ -7863,9 +8175,9 @@ yargs@^14.2.3: yargs-parser "^15.0.1" yarn@^1.22.4: - version "1.22.4" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.4.tgz#01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e" - integrity sha512-oYM7hi/lIWm9bCoDMEWgffW8aiNZXCWeZ1/tGy0DWrN6vmzjCXIKu2Y21o8DYVBUtiktwKcNoxyGl/2iKLUNGA== + version "1.22.10" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c" + integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA== yn@3.1.1: version "3.1.1" From 9f25cc6d5d1c08cc7d1a4fd583a491680da9cd6a Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 15 Oct 2020 17:47:20 -0500 Subject: [PATCH 04/82] Move providers from `app` to `routes` --- src/node/entry.ts | 12 ++++++------ src/node/{app => routes}/health.ts | 0 src/node/{app => routes}/login.ts | 0 src/node/{app => routes}/proxy.ts | 0 src/node/{app => routes}/static.ts | 0 src/node/{app => routes}/update.ts | 0 src/node/{app => routes}/vscode.ts | 0 test/update.test.ts | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename src/node/{app => routes}/health.ts (100%) rename src/node/{app => routes}/login.ts (100%) rename src/node/{app => routes}/proxy.ts (100%) rename src/node/{app => routes}/static.ts (100%) rename src/node/{app => routes}/update.ts (100%) rename src/node/{app => routes}/vscode.ts (100%) diff --git a/src/node/entry.ts b/src/node/entry.ts index 826d9a484..630e4b0b7 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -5,12 +5,12 @@ import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { plural } from "../common/util" -import { HealthHttpProvider } from "./app/health" -import { LoginHttpProvider } from "./app/login" -import { ProxyHttpProvider } from "./app/proxy" -import { StaticHttpProvider } from "./app/static" -import { UpdateHttpProvider } from "./app/update" -import { VscodeHttpProvider } from "./app/vscode" +import { HealthHttpProvider } from "./routes/health" +import { LoginHttpProvider } from "./routes/login" +import { ProxyHttpProvider } from "./routes/proxy" +import { StaticHttpProvider } from "./routes/static" +import { UpdateHttpProvider } from "./routes/update" +import { VscodeHttpProvider } from "./routes/vscode" import { DefaultedArgs, optionDescriptions, diff --git a/src/node/app/health.ts b/src/node/routes/health.ts similarity index 100% rename from src/node/app/health.ts rename to src/node/routes/health.ts diff --git a/src/node/app/login.ts b/src/node/routes/login.ts similarity index 100% rename from src/node/app/login.ts rename to src/node/routes/login.ts diff --git a/src/node/app/proxy.ts b/src/node/routes/proxy.ts similarity index 100% rename from src/node/app/proxy.ts rename to src/node/routes/proxy.ts diff --git a/src/node/app/static.ts b/src/node/routes/static.ts similarity index 100% rename from src/node/app/static.ts rename to src/node/routes/static.ts diff --git a/src/node/app/update.ts b/src/node/routes/update.ts similarity index 100% rename from src/node/app/update.ts rename to src/node/routes/update.ts diff --git a/src/node/app/vscode.ts b/src/node/routes/vscode.ts similarity index 100% rename from src/node/app/vscode.ts rename to src/node/routes/vscode.ts diff --git a/test/update.test.ts b/test/update.test.ts index 9e27eefaf..093429be3 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -2,8 +2,8 @@ import * as assert from "assert" import * as fs from "fs-extra" import * as http from "http" import * as path from "path" -import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update" import { AuthType } from "../src/node/cli" +import { LatestResponse, UpdateHttpProvider } from "../src/node/routes/update" import { SettingsProvider, UpdateSettings } from "../src/node/settings" import { tmpdir } from "../src/node/util" From 8e93e281628a09d66cdc131cf31816de8a529bd9 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 16 Oct 2020 12:31:22 -0500 Subject: [PATCH 05/82] Strip config file password from debug log --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 7f44f9ea5..a5757aa6b 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -334,7 +334,7 @@ export const parse = ( throw new Error("--cert-key is missing") } - logger.debug("parsed command line", field("args", args)) + logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })]) return args } From 71dc5c75427c653a472b2cad6a07495e0827e9bb Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 16 Oct 2020 12:43:49 -0500 Subject: [PATCH 06/82] Switch to Express It doesn't do anything yet. --- src/node/app.ts | 57 ++++++++++++++++++++++++++++++++++++++++ src/node/entry.ts | 65 ++++++++++++---------------------------------- src/node/plugin.ts | 20 +++++++------- 3 files changed, 84 insertions(+), 58 deletions(-) create mode 100644 src/node/app.ts diff --git a/src/node/app.ts b/src/node/app.ts new file mode 100644 index 000000000..7e5e7a2ef --- /dev/null +++ b/src/node/app.ts @@ -0,0 +1,57 @@ +import { logger } from "@coder/logger" +import express, { Express } from "express" +import { promises as fs } from "fs" +import http from "http" +import * as httpolyglot from "httpolyglot" +import { DefaultedArgs } from "./cli" + +/** + * Create an Express app and an HTTP/S server to serve it. + */ +export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Server]> => { + const app = express() + + const server = args.cert + ? httpolyglot.createServer( + { + cert: args.cert && (await fs.readFile(args.cert.value)), + key: args["cert-key"] && (await fs.readFile(args["cert-key"])), + }, + app, + ) + : http.createServer(app) + + await new Promise(async (resolve, reject) => { + server.on("error", reject) + if (args.socket) { + try { + await fs.unlink(args.socket) + } catch (error) { + if (error.code !== "ENOENT") { + logger.error(error.message) + } + } + server.listen(args.socket, resolve) + } else { + // [] is the correct format when using :: but Node errors with them. + server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve) + } + }) + + return [app, server] +} + +/** + * Get the address of a server as a string (protocol not included) while + * ensuring there is one (will throw if there isn't). + */ +export const ensureAddress = (server: http.Server): string => { + const addr = server.address() + if (!addr) { + throw new Error("server has no address") + } + if (typeof addr !== "string") { + return `${addr.address}:${addr.port}` + } + return addr +} diff --git a/src/node/entry.ts b/src/node/entry.ts index 630e4b0b7..e8c6b4e1f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -5,13 +5,9 @@ import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { plural } from "../common/util" -import { HealthHttpProvider } from "./routes/health" -import { LoginHttpProvider } from "./routes/login" -import { ProxyHttpProvider } from "./routes/proxy" -import { StaticHttpProvider } from "./routes/static" -import { UpdateHttpProvider } from "./routes/update" -import { VscodeHttpProvider } from "./routes/vscode" +import { createApp, ensureAddress } from "./app" import { + AuthType, DefaultedArgs, optionDescriptions, parse, @@ -21,9 +17,8 @@ import { shouldRunVsCodeCli, } from "./cli" import { coderCloudBind } from "./coder-cloud" -import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" -import { hash, humanPath, open } from "./util" +import { humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" let pkg: { version?: string; commit?: string } = {} @@ -117,65 +112,39 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st } 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) { throw new Error("Please pass in a password via the config file or $PASSWORD") } - - // Spawn the main HTTP server. - const options: HttpServerOptions = { - auth: args.auth, - commit, - host: args.host, - // The hash does not add any actual security but we do it for obfuscation purposes. - password: args.password ? hash(args.password) : undefined, - port: args.port, - proxyDomains: args["proxy-domain"], - socket: args.socket, - cert: args.cert && args.cert.value, - certKey: args["cert-key"], - } - - if (options.cert && !options.certKey) { - throw new Error("--cert-key is missing") - } - - const httpServer = new HttpServer(options) - httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args) - httpServer.registerHttpProvider("/update", UpdateHttpProvider, false) - httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) - httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, args.usingEnvPassword) - httpServer.registerHttpProvider("/static", StaticHttpProvider) - httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart) - - await loadPlugins(httpServer, args) - ipcMain.onDispose(() => { - httpServer.dispose().then((errors) => { - errors.forEach((error) => logger.error(error.message)) - }) + // TODO: register disposables }) - logger.info(`code-server ${version} ${commit}`) - logger.info(`Using config file ${humanPath(args.config)}`) + const [app, server] = await createApp(args) + const serverAddress = ensureAddress(server) - const serverAddress = await httpServer.listen() + // TODO: register routes + await loadPlugins(app, 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 { logger.info(` - Using password from ${humanPath(args.config)}`) } - logger.info(" - To disable use `--auth none`") } else { - logger.info(` - No authentication ${args.link ? "(disabled by --link)" : ""}`) + logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`) } - if (httpServer.protocol === "https") { + if (args.cert) { logger.info( args.cert && args.cert.value ? ` - Using provided certificate and key for HTTPS` @@ -192,7 +161,7 @@ const main = async (args: DefaultedArgs): Promise => { if (args.link) { try { - await coderCloudBind(serverAddress!, args.link.value) + await coderCloudBind(serverAddress, args.link.value) logger.info(" - Connected to cloud agent") } catch (err) { logger.error(err.message) @@ -200,7 +169,7 @@ const main = async (args: DefaultedArgs): Promise => { } } - if (serverAddress && !options.socket && args.open) { + if (serverAddress && !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") await open(openAddress).catch((error: Error) => { diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 7469f317d..20c19d3e7 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,14 +1,14 @@ import { field, logger } from "@coder/logger" +import { Express } from "express" import * as fs from "fs" import * as path from "path" import * as util from "util" import { Args } from "./cli" -import { HttpServer } from "./http" import { paths } from "./util" /* eslint-disable @typescript-eslint/no-var-requires */ -export type Activate = (httpServer: HttpServer, args: Args) => void +export type Activate = (app: Express, args: Args) => void /** * Plugins must implement this interface. @@ -30,10 +30,10 @@ require("module")._load = function (request: string, parent: object, isMain: boo /** * Load a plugin and run its activation function. */ -const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise => { +const loadPlugin = async (pluginPath: string, app: Express, args: Args): Promise => { try { const plugin: Plugin = require(pluginPath) - plugin.activate(httpServer, args) + plugin.activate(app, args) const packageJson = require(path.join(pluginPath, "package.json")) logger.debug( @@ -50,12 +50,12 @@ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args /** * Load all plugins in the specified directory. */ -const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise => { +const _loadPlugins = async (pluginDir: string, app: Express, args: Args): Promise => { try { const files = await util.promisify(fs.readdir)(pluginDir, { withFileTypes: true, }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args))) + await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), app, args))) } catch (error) { if (error.code !== "ENOENT") { logger.warn(error.message) @@ -68,17 +68,17 @@ const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Arg * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by * `CS_PLUGIN` (also colon-separated). */ -export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { +export const loadPlugins = async (app: Express, args: Args): Promise => { const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` const plugin = process.env.CS_PLUGIN || "" await Promise.all([ // Built-in plugins. - _loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args), + _loadPlugins(path.resolve(__dirname, "../../plugins"), app, args), // User-added plugins. ...pluginPath .split(":") .filter((p) => !!p) - .map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)), + .map((dir) => _loadPlugins(path.resolve(dir), app, args)), // Individual plugins so you don't have to symlink or move them into a // directory specifically for plugins. This lets you load plugins that are // on the same level as other directories that are not plugins (if you tried @@ -87,6 +87,6 @@ export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise !!p) - .map((dir) => loadPlugin(path.resolve(dir), httpServer, args)), + .map((dir) => loadPlugin(path.resolve(dir), app, args)), ]) } From 4b6cbacbad522ceb259845bc6d315aa8310a2945 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 16 Oct 2020 14:45:49 -0500 Subject: [PATCH 07/82] Add file for global constants --- src/node/constants.ts | 13 +++++++++++++ src/node/entry.ts | 11 +---------- 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 src/node/constants.ts diff --git a/src/node/constants.ts b/src/node/constants.ts new file mode 100644 index 000000000..d6ba953ea --- /dev/null +++ b/src/node/constants.ts @@ -0,0 +1,13 @@ +import { logger } from "@coder/logger" +import * as path from "path" + +let pkg: { version?: string; commit?: string } = {} +try { + pkg = require("../../package.json") +} catch (error) { + logger.warn(error.message) +} + +export const version = pkg.version || "development" +export const commit = pkg.commit || "development" +export const rootPath = path.resolve(__dirname, "../..") diff --git a/src/node/entry.ts b/src/node/entry.ts index e8c6b4e1f..ce0d2980f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -17,20 +17,11 @@ import { shouldRunVsCodeCli, } from "./cli" import { coderCloudBind } from "./coder-cloud" +import { commit, version } from "./constants" import { loadPlugins } from "./plugin" import { humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" -let pkg: { version?: string; commit?: string } = {} -try { - pkg = require("../../package.json") -} catch (error) { - logger.warn(error.message) -} - -const version = pkg.version || "development" -const commit = pkg.commit || "development" - 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"), [], { From 112eda46052ef5ea72b71b68817e8c00121a5140 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 20 Oct 2020 18:05:58 -0500 Subject: [PATCH 08/82] Convert routes to Express --- .eslintrc.yaml | 2 + ci/dev/vscode.patch | 4 +- package.json | 5 + src/common/http.ts | 2 +- src/node/app.ts | 3 + src/node/entry.ts | 7 +- src/node/http.ts | 1092 ++++++++----------------------------- src/node/proxy.ts | 73 +++ src/node/routes/health.ts | 28 +- src/node/routes/index.ts | 123 +++++ src/node/routes/login.ts | 210 +++---- src/node/routes/proxy.ts | 76 +-- src/node/routes/static.ts | 96 ++-- src/node/routes/update.ts | 196 +------ src/node/routes/vscode.ts | 304 +++-------- src/node/settings.ts | 4 +- src/node/update.ts | 133 +++++ src/node/vscode.ts | 150 +++++ test/update.test.ts | 25 +- yarn.lock | 21 +- 20 files changed, 1031 insertions(+), 1523 deletions(-) create mode 100644 src/node/proxy.ts create mode 100644 src/node/routes/index.ts create mode 100644 src/node/update.ts create mode 100644 src/node/vscode.ts diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 92657d629..231c2dbca 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -31,6 +31,8 @@ rules: import/order: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] no-async-promise-executor: off + # This isn't a real module, just types, which apparently doesn't resolve. + import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }] settings: # Does not work with CommonJS unfortunately. diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index b205aa83b..9d759649e 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1318,7 +1318,7 @@ index 0000000000000000000000000000000000000000..56331ff1fc32bbd82e769aaecb551e42 +require('../../bootstrap-amd').load('vs/server/entry'); diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts new file mode 100644 -index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087dcee0a4e2 +index 0000000000000000000000000000000000000000..6ce56bec114a6d8daf5dd3ded945ea78fc72a5c6 --- /dev/null +++ b/src/vs/server/ipc.d.ts @@ -0,0 +1,131 @@ @@ -1336,7 +1336,7 @@ index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087d + options: VscodeOptions; +} + -+export type Query = { [key: string]: string | string[] | undefined }; ++export type Query = { [key: string]: string | string[] | undefined | Query | Query[] }; + +export interface SocketMessage { + type: 'socket'; diff --git a/package.json b/package.json index e6197065f..187052f8d 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ }, "main": "out/node/entry.js", "devDependencies": { + "@types/body-parser": "^1.19.0", + "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.8", "@types/fs-extra": "^8.0.1", "@types/http-proxy": "^1.17.4", @@ -67,6 +69,8 @@ }, "dependencies": { "@coder/logger": "1.1.16", + "body-parser": "^1.19.0", + "cookie-parser": "^1.4.5", "env-paths": "^2.2.0", "express": "^4.17.1", "fs-extra": "^9.0.1", @@ -75,6 +79,7 @@ "js-yaml": "^3.13.1", "limiter": "^1.1.5", "pem": "^1.14.2", + "qs": "6.7.0", "rotating-file-stream": "^2.1.1", "safe-buffer": "^5.1.1", "safe-compare": "^1.1.4", diff --git a/src/common/http.ts b/src/common/http.ts index 4749247d7..5279bf44f 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -9,7 +9,7 @@ export enum HttpCode { } export class HttpError extends Error { - public constructor(message: string, public readonly code: number, public readonly details?: object) { + public constructor(message: string, public readonly status: number, public readonly details?: object) { super(message) this.name = this.constructor.name } diff --git a/src/node/app.ts b/src/node/app.ts index 7e5e7a2ef..5fde27675 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -4,6 +4,7 @@ import { promises as fs } from "fs" import http from "http" import * as httpolyglot from "httpolyglot" import { DefaultedArgs } from "./cli" +import { handleUpgrade } from "./http" /** * Create an Express app and an HTTP/S server to serve it. @@ -38,6 +39,8 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Ser } }) + handleUpgrade(app, server) + return [app, server] } diff --git a/src/node/entry.ts b/src/node/entry.ts index ce0d2980f..30dd93d8f 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -18,7 +18,7 @@ import { } from "./cli" import { coderCloudBind } from "./coder-cloud" import { commit, version } from "./constants" -import { loadPlugins } from "./plugin" +import { register } from "./routes" import { humanPath, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" @@ -111,15 +111,14 @@ const main = async (args: DefaultedArgs): Promise => { if (args.auth === AuthType.Password && !args.password) { throw new Error("Please pass in a password via the config file or $PASSWORD") } + ipcMain.onDispose(() => { // TODO: register disposables }) const [app, server] = await createApp(args) const serverAddress = ensureAddress(server) - - // TODO: register routes - await loadPlugins(app, args) + await register(app, server, args) logger.info(`Using config file ${humanPath(args.config)}`) logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`) diff --git a/src/node/http.ts b/src/node/http.ts index 4aa6dc067..960669100 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,885 +1,253 @@ import { field, logger } from "@coder/logger" -import * as fs from "fs-extra" +import * as express from "express" +import * as expressCore from "express-serve-static-core" import * as http from "http" -import proxy from "http-proxy" -import * as httpolyglot from "httpolyglot" -import * as https from "https" import * as net from "net" -import * as path from "path" -import * as querystring from "querystring" +import qs from "qs" import safeCompare from "safe-compare" -import { Readable } from "stream" -import * as tls from "tls" -import * as url from "url" import { HttpCode, HttpError } from "../common/http" -import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util" +import { normalize, Options } from "../common/util" import { AuthType } from "./cli" +import { commit, rootPath } from "./constants" import { Heart } from "./heart" -import { SocketProxyProvider } from "./socket" -import { getMediaMime, paths } from "./util" +import { hash } from "./util" -export type Cookies = { [key: string]: string[] | undefined } -export type PostData = { [key: string]: string | string[] | undefined } - -interface ProxyRequest extends http.IncomingMessage { - base?: string -} - -interface AuthPayload extends Cookies { - key?: string[] -} - -export type Query = { [key: string]: string | string[] | undefined } - -export interface ProxyOptions { - /** - * A path to strip from from the beginning of the request before proxying - */ - strip?: string - /** - * A path to add to the beginning of the request before proxying. - */ - prepend?: string - /** - * The port to proxy. - */ - port: string -} - -export interface HttpResponse { - /* - * Whether to set cache-control headers for this response. - */ - cache?: boolean - /** - * If the code cannot be determined automatically set it here. The - * defaults are 302 for redirects and 200 for successful requests. For errors - * you should throw an HttpError and include the code there. If you - * use Error it will default to 404 for ENOENT and EISDIR and 500 otherwise. - */ - code?: number - /** - * Content to write in the response. Mutually exclusive with stream. - */ - content?: T - /** - * Cookie to write with the response. - * NOTE: Cookie paths must be absolute. The default is /. - */ - cookie?: { key: string; value: string; path?: string } - /** - * Used to automatically determine the appropriate mime type. - */ - filePath?: string - /** - * Additional headers to include. - */ - headers?: http.OutgoingHttpHeaders - /** - * If the mime type cannot be determined automatically set it here. - */ - mime?: string - /** - * Redirect to this path. This is constructed against the site base (not the - * provider's base). - */ - redirect?: string - /** - * Stream this to the response. Mutually exclusive with content. - */ - stream?: Readable - /** - * Query variables to add in addition to current ones when redirecting. Use - * `undefined` to remove a query variable. - */ - query?: Query - /** - * Indicates the request should be proxied. - */ - proxy?: ProxyOptions -} - -export interface WsResponse { - /** - * Indicates the web socket should be proxied. - */ - proxy?: ProxyOptions +export interface Locals { + heart: Heart } /** - * Use when you need to run search and replace on a file's content before - * sending it. + * Replace common variable strings in HTML templates. */ -export interface HttpStringFileResponse extends HttpResponse { - content: string - filePath: string -} - -export interface RedirectResponse extends HttpResponse { - redirect: string -} - -export interface HttpServerOptions { - readonly auth?: AuthType - readonly cert?: string - readonly certKey?: string - readonly commit?: string - readonly host?: string - readonly password?: string - readonly port?: number - readonly proxyDomains: string[] - readonly socket?: string -} - -export interface Route { - /** - * Provider base path part (for /provider/base/path it would be /provider). - */ - providerBase: string - /** - * Base path part (for /provider/base/path it would be /base). - */ - base: string - /** - * Remaining part of the route after factoring out the base and provider base - * (for /provider/base/path it would be /path). It can be blank. - */ - requestPath: string - /** - * Query variables included in the request. - */ - query: querystring.ParsedUrlQuery - /** - * Normalized version of `originalPath`. - */ - fullPath: string - /** - * Original path of the request without any modifications. - */ - originalPath: string -} - -interface ProviderRoute extends Route { - provider: HttpProvider -} - -export interface HttpProviderOptions { - readonly auth: AuthType - readonly commit: string - readonly password?: string +export const replaceTemplates = ( + req: express.Request, + content: string, + extraOpts?: Omit, +): string => { + const base = relativeRoot(req) + const options: Options = { + base, + csStaticBase: base + "/static/" + commit + rootPath, + logLevel: logger.level, + ...extraOpts, + } + return content + .replace(/{{TO}}/g, (typeof req.query.to === "string" && req.query.to) || "/") + .replace(/{{BASE}}/g, options.base) + .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) + .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) } /** - * Provides HTTP responses. This abstract class provides some helpers for - * interpreting, creating, and authenticating responses. + * Throw an error if not authorized. */ -export abstract class HttpProvider { - protected readonly rootPath = path.resolve(__dirname, "../..") - - public constructor(protected readonly options: HttpProviderOptions) {} - - public async dispose(): Promise { - // No default behavior. +export const ensureAuthenticated = (req: express.Request): void => { + if (!authenticated(req)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) } - - /** - * Handle web sockets on the registered endpoint. Normally the provider - * handles the request itself but it can return a response when necessary. The - * default is to throw a 404. - */ - public handleWebSocket( - /* eslint-disable @typescript-eslint/no-unused-vars */ - _route: Route, - _request: http.IncomingMessage, - _socket: net.Socket, - _head: Buffer, - /* eslint-enable @typescript-eslint/no-unused-vars */ - ): Promise { - throw new HttpError("Not found", HttpCode.NotFound) - } - - /** - * Handle requests to the registered endpoint. - */ - public abstract handleRequest(route: Route, request: http.IncomingMessage): Promise - - /** - * Get the base relative to the provided route. For each slash we need to go - * up a directory. For example: - * / => . - * /foo => . - * /foo/ => ./.. - * /foo/bar => ./.. - * /foo/bar/ => ./../.. - */ - public base(route: Route): string { - const depth = (route.originalPath.match(/\//g) || []).length - return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) - } - - /** - * Get error response. - */ - public async getErrorRoot(route: Route, title: string, header: string, body: string): Promise { - const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/error.html") - response.content = response.content - .replace(/{{ERROR_TITLE}}/g, title) - .replace(/{{ERROR_HEADER}}/g, header) - .replace(/{{ERROR_BODY}}/g, body) - return this.replaceTemplates(route, response) - } - - /** - * Replace common templates strings. - */ - protected replaceTemplates( - route: Route, - response: HttpStringFileResponse, - extraOptions?: Omit, - ): HttpStringFileResponse { - const base = this.base(route) - const options: Options = { - base, - csStaticBase: base + "/static/" + this.options.commit + this.rootPath, - logLevel: logger.level, - ...extraOptions, - } - response.content = response.content - .replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard") - .replace(/{{BASE}}/g, options.base) - .replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) - .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) - return response - } - - protected get isDev(): boolean { - return this.options.commit === "development" - } - - /** - * Get a file resource. - * TODO: Would a stream be faster, at least for large files? - */ - protected async getResource(...parts: string[]): Promise { - const filePath = path.join(...parts) - return { content: await fs.readFile(filePath), filePath } - } - - /** - * Get a file resource as a string. - */ - protected async getUtf8Resource(...parts: string[]): Promise { - const filePath = path.join(...parts) - return { content: await fs.readFile(filePath, "utf8"), filePath } - } - - /** - * Helper to error on invalid methods (default GET). - */ - protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void { - const check = arrayify(method || "GET") - if (!request.method || !check.includes(request.method)) { - throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest) - } - } - - /** - * Helper to error if not authorized. - */ - public ensureAuthenticated(request: http.IncomingMessage): void { - if (!this.authenticated(request)) { - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - } - - /** - * Use the first query value or the default if there isn't one. - */ - protected queryOrDefault(value: string | string[] | undefined, def: string): string { - if (Array.isArray(value)) { - value = value[0] - } - return typeof value !== "undefined" ? value : def - } - - /** - * Return the provided password value if the payload contains the right - * password otherwise return false. If no payload is specified use cookies. - */ - public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean { - switch (this.options.auth) { - case AuthType.None: - return true - case AuthType.Password: - if (typeof payload === "undefined") { - payload = this.parseCookies(request) - } - if (this.options.password && payload.key) { - for (let i = 0; i < payload.key.length; ++i) { - if (safeCompare(payload.key[i], this.options.password)) { - return payload.key[i] - } - } - } - return false - default: - throw new Error(`Unsupported auth type ${this.options.auth}`) - } - } - - /** - * Parse POST data. - */ - protected getData(request: http.IncomingMessage): Promise { - return request.method === "POST" || request.method === "DELETE" - ? new Promise((resolve, reject) => { - let body = "" - const onEnd = (): void => { - off() // eslint-disable-line @typescript-eslint/no-use-before-define - resolve(body || undefined) - } - const onError = (error: Error): void => { - off() // eslint-disable-line @typescript-eslint/no-use-before-define - reject(error) - } - const onData = (d: Buffer): void => { - body += d - if (body.length > 1e6) { - onError(new HttpError("Payload is too large", HttpCode.LargePayload)) - request.connection.destroy() - } - } - const off = (): void => { - request.off("error", onError) - request.off("data", onError) - request.off("end", onEnd) - } - request.on("error", onError) - request.on("data", onData) - request.on("end", onEnd) - }) - : Promise.resolve(undefined) - } - - /** - * Parse cookies. - */ - protected parseCookies(request: http.IncomingMessage): T { - const cookies: { [key: string]: string[] } = {} - if (request.headers.cookie) { - request.headers.cookie.split(";").forEach((keyValue) => { - const [key, value] = split(keyValue, "=") - if (!cookies[key]) { - cookies[key] = [] - } - cookies[key].push(decodeURI(value)) - }) - } - return cookies as T - } - - /** - * Return true if the route is for the root page. For example /base, /base/, - * or /base/index.html but not /base/path or /base/file.js. - */ - protected isRoot(route: Route): boolean { - return !route.requestPath || route.requestPath === "/index.html" - } -} - -export interface HttpProvider0 { - new (options: HttpProviderOptions): T -} - -export interface HttpProvider1 { - new (options: HttpProviderOptions, a1: A1): T -} - -export interface HttpProvider2 { - new (options: HttpProviderOptions, a1: A1, a2: A2): T -} - -export interface HttpProvider3 { - new (options: HttpProviderOptions, a1: A1, a2: A2, a3: A3): T } /** - * An HTTP server. Its main role is to route incoming HTTP requests to the - * appropriate provider for that endpoint then write out the response. It also - * covers some common use cases like redirects and caching. + * Return true if authenticated via cookies. */ -export class HttpServer { - protected readonly server: http.Server | https.Server - private listenPromise: Promise | undefined - public readonly protocol: "http" | "https" - private readonly providers = new Map() - public readonly heart: Heart - private readonly socketProvider = new SocketProxyProvider() - - /** - * Provides the actual proxying functionality. - */ - private readonly proxy = proxy.createProxyServer({}) - - public constructor(private readonly options: HttpServerOptions) { - this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => { - const connections = await this.getConnections() - logger.trace(plural(connections, `${connections} active connection`)) - return connections !== 0 - }) - this.protocol = this.options.cert ? "https" : "http" - if (this.protocol === "https") { - this.server = httpolyglot.createServer( - { - cert: this.options.cert && fs.readFileSync(this.options.cert), - key: this.options.certKey && fs.readFileSync(this.options.certKey), - }, - this.onRequest, - ) - } else { - this.server = http.createServer(this.onRequest) - } - this.proxy.on("error", (error, _request, response) => { - response.writeHead(HttpCode.ServerError) - response.end(error.message) - }) - // Intercept the response to rewrite absolute redirects against the base path. - this.proxy.on("proxyRes", (response, request: ProxyRequest) => { - if (response.headers.location && response.headers.location.startsWith("/") && request.base) { - response.headers.location = request.base + response.headers.location - } - }) - } - - /** - * Stop and dispose everything. Return an array of disposal errors. - */ - public async dispose(): Promise { - this.socketProvider.stop() - const providers = Array.from(this.providers.values()) - // Catch so all the errors can be seen rather than just the first one. - const responses = await Promise.all(providers.map((p) => p.dispose().catch((e) => e))) - return responses.filter((r): r is Error => typeof r !== "undefined") - } - - public async getConnections(): Promise { - return new Promise((resolve, reject) => { - this.server.getConnections((error, count) => { - return error ? reject(error) : resolve(count) - }) - }) - } - - /** - * Register a provider for a top-level endpoint. - */ - public registerHttpProvider(endpoint: string | string[], provider: HttpProvider0): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider1, - a1: A1, - ): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider2, - a1: A1, - a2: A2, - ): T - public registerHttpProvider( - endpoint: string | string[], - provider: HttpProvider3, - a1: A1, - a2: A2, - a3: A3, - ): T - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void { - const p = new provider( - { - auth: this.options.auth || AuthType.None, - commit: this.options.commit, - password: this.options.password, - }, - ...args, - ) - const endpoints = arrayify(endpoint).map(trimSlashes) - endpoints.forEach((endpoint) => { - if (/\//.test(endpoint)) { - throw new Error(`Only top-level endpoints are supported (got ${endpoint})`) - } - const existingProvider = this.providers.get(`/${endpoint}`) - this.providers.set(`/${endpoint}`, p) - if (existingProvider) { - logger.debug(`Overridding existing /${endpoint} provider`) - // If the existing provider isn't registered elsewhere we can dispose. - if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) { - logger.debug(`Disposing existing /${endpoint} provider`) - existingProvider.dispose() - } - } - }) - } - - /** - * Start listening on the specified port. - */ - public listen(): Promise { - if (!this.listenPromise) { - this.listenPromise = new Promise(async (resolve, reject) => { - this.server.on("error", reject) - this.server.on("upgrade", this.onUpgrade) - const onListen = (): void => resolve(this.address()) - if (this.options.socket) { - try { - await fs.unlink(this.options.socket) - } catch (err) { - if (err.code !== "ENOENT") { - logger.warn(err.message) - } - } - this.server.listen(this.options.socket, onListen) - } else if (this.options.host) { - // [] is the correct format when using :: but Node errors with them. - this.server.listen(this.options.port, this.options.host.replace(/^\[|\]$/g, ""), onListen) - } else { - this.server.listen(this.options.port, onListen) - } - }) - } - return this.listenPromise - } - - /** - * The *local* address of the server. - */ - public address(): string | null { - const address = this.server.address() - const endpoint = - typeof address !== "string" && address !== null - ? (address.address === "::" ? "localhost" : address.address) + ":" + address.port - : address - return endpoint && `${this.protocol}://${endpoint}` - } - - private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise => { - const route = this.parseUrl(request) - if (route.providerBase !== "/healthz") { - this.heart.beat() - } - const write = (payload: HttpResponse): void => { - response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { - "Content-Type": payload.mime || getMediaMime(payload.filePath), - ...(payload.redirect ? { Location: this.constructRedirect(request, route, payload as RedirectResponse) } : {}), - ...(request.headers["service-worker"] ? { "Service-Worker-Allowed": route.provider.base(route) } : {}), - ...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}), - ...(payload.cookie - ? { - "Set-Cookie": [ - `${payload.cookie.key}=${payload.cookie.value}`, - `Path=${normalize(payload.cookie.path || "/", true)}`, - this.getCookieDomain(request.headers.host || ""), - // "HttpOnly", - "SameSite=lax", - ] - .filter((l) => !!l) - .join(";"), - } - : {}), - ...payload.headers, - }) - if (payload.stream) { - payload.stream.on("error", (error: NodeJS.ErrnoException) => { - response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError) - response.end(error.message) - }) - payload.stream.on("close", () => response.end()) - payload.stream.pipe(response) - } else if (typeof payload.content === "string" || payload.content instanceof Buffer) { - response.end(payload.content) - } else if (payload.content && typeof payload.content === "object") { - response.end(JSON.stringify(payload.content)) - } else { - response.end() - } - } - - try { - const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request)) - if (payload.proxy) { - this.doProxy(route, request, response, payload.proxy) - } else { - write(payload) - } - } catch (error) { - let e = error - if (error.code === "ENOENT" || error.code === "EISDIR") { - e = new HttpError("Not found", HttpCode.NotFound) - } - const code = typeof e.code === "number" ? e.code : HttpCode.ServerError - logger.debug("Request error", field("url", request.url), field("code", code), field("error", error)) - if (code >= HttpCode.ServerError) { - logger.error(error.stack) - } - if (request.headers["content-type"] === "application/json") { - write({ - code, - mime: "application/json", - content: { - error: e.message, - ...(e.details || {}), - }, - }) - } else { - write({ - code, - ...(await route.provider.getErrorRoot(route, code, code, e.message)), - }) - } - } - } - - /** - * Handle requests that are always in effect no matter what provider is - * registered at the route. - */ - private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise { - // If we're handling TLS ensure all requests are redirected to HTTPS. - if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) { - return { redirect: route.fullPath } - } - - // Return robots.txt. - if (route.fullPath === "/robots.txt") { - const filePath = path.resolve(__dirname, "../../src/browser/robots.txt") - return { content: await fs.readFile(filePath), filePath } - } - - // Handle proxy domains. - return this.maybeProxy(route, request) - } - - /** - * Given a path that goes from the base, construct a relative redirect URL - * that will get you there considering that the app may be served from an - * unknown base path. If handling TLS, also ensure HTTPS. - */ - private constructRedirect(request: http.IncomingMessage, route: ProviderRoute, payload: RedirectResponse): string { - const query = { - ...route.query, - ...(payload.query || {}), - } - - Object.keys(query).forEach((key) => { - if (typeof query[key] === "undefined") { - delete query[key] - } - }) - - const secure = (request.connection as tls.TLSSocket).encrypted - const redirect = - (this.options.cert && !secure ? `${this.protocol}://${request.headers.host}/` : "") + - normalize(`${route.provider.base(route)}/${payload.redirect}`, true) + - (Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "") - logger.debug("redirecting", field("secure", !!secure), field("from", request.url), field("to", redirect)) - return redirect - } - - private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise => { - try { - this.heart.beat() - socket.on("error", () => socket.destroy()) - - if (this.options.cert && !(socket as tls.TLSSocket).encrypted) { - throw new HttpError("HTTP websocket", HttpCode.BadRequest) - } - - if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") { - throw new HttpError("HTTP/1.1 400 Bad Request", HttpCode.BadRequest) - } - - const route = this.parseUrl(request) - if (!route.provider) { - throw new HttpError("Not found", HttpCode.NotFound) - } - - // The socket proxy is so we can pass them to child processes (TLS sockets - // can't be transferred so we need an in-between). - const socketProxy = await this.socketProvider.createProxy(socket) - const payload = - this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head)) - if (payload && payload.proxy) { - this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy) - } - } catch (error) { - socket.destroy(error) - logger.warn(`discarding socket connection: ${error.message}`) - } - } - - /** - * Parse a request URL so we can route it. - */ - private parseUrl(request: http.IncomingMessage): ProviderRoute { - const parse = (fullPath: string): { base: string; requestPath: string } => { - const match = fullPath.match(/^(\/?[^/]*)(.*)$/) - let [, /* ignore */ base, requestPath] = match ? match.map((p) => p.replace(/\/+$/, "")) : ["", "", ""] - if (base.indexOf(".") !== -1) { - // Assume it's a file at the root. - requestPath = base - base = "/" - } else if (base === "") { - // Happens if it's a plain `domain.com`. - base = "/" - } - return { base, requestPath } - } - - const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}, pathname: "" } - const originalPath = parsedUrl.pathname || "/" - const fullPath = normalize(originalPath, true) - const { base, requestPath } = parse(fullPath) - - // Providers match on the path after their base so we need to account for - // that by shifting the next base out of the request path. - let provider = this.providers.get(base) - if (base !== "/" && provider) { - return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath } - } - - // Fall back to the top-level provider. - provider = this.providers.get("/") - if (!provider) { - throw new Error(`No provider for ${base}`) - } - return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath } - } - - /** - * Proxy a request to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: http.ServerResponse, - options: ProxyOptions, - ): void - /** - * Proxy a web socket to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: { socket: net.Socket; head: Buffer }, - options: ProxyOptions, - ): void - /** - * Proxy a request or web socket to the target. - */ - private doProxy( - route: Route, - request: http.IncomingMessage, - response: http.ServerResponse | { socket: net.Socket; head: Buffer }, - options: ProxyOptions, - ): void { - const port = parseInt(options.port, 10) - if (isNaN(port)) { - throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest) - } - - // REVIEW: Absolute redirects need to be based on the subpath but I'm not - // sure how best to get this information to the `proxyRes` event handler. - // For now I'm sticking it on the request object which is passed through to - // the event. - ;(request as ProxyRequest).base = options.strip - - const isHttp = response instanceof http.ServerResponse - const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath - const path = normalize("/" + (options.prepend || "") + "/" + base, true) - const proxyOptions: proxy.ServerOptions = { - changeOrigin: true, - ignorePath: true, - target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${ - Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : "" - }`, - ws: !isHttp, - } - - if (response instanceof http.ServerResponse) { - this.proxy.web(request, response, proxyOptions) - } else { - this.proxy.ws(request, response.socket, response.head, proxyOptions) - } - } - - /** - * Get the value that should be used for setting a cookie domain. This will - * allow the user to authenticate only once. This will use the highest level - * domain (e.g. `coder.com` over `test.coder.com` if both are specified). - */ - private getCookieDomain(host: string): string | undefined { - const idx = host.lastIndexOf(":") - host = idx !== -1 ? host.substring(0, idx) : host - if ( - // Might be blank/missing, so there's nothing more to do. - !host || - // IP addresses can't have subdomains so there's no value in setting the - // domain for them. Assume anything with a : is ipv6 (valid domain name - // characters are alphanumeric or dashes). - host.includes(":") || - // Assume anything entirely numbers and dots is ipv4 (currently tlds - // cannot be entirely numbers). - !/[^0-9.]/.test(host) || - // localhost subdomains don't seem to work at all (browser bug?). - host.endsWith(".localhost") || - // It might be localhost (or an IP, see above) if it's a proxy and it - // isn't setting the host header to match the access domain. - host === "localhost" - ) { - logger.debug("no valid cookie doman", field("host", host)) - return undefined - } - - this.options.proxyDomains.forEach((domain) => { - if (host.endsWith(domain) && domain.length < host.length) { - host = domain - } - }) - - logger.debug("got cookie doman", field("host", host)) - return host ? `Domain=${host}` : undefined - } - - /** - * Return a response if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. - * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. - * - * Throw an error if proxying but the user isn't authenticated. - */ - public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined { - // Split into parts. - const host = request.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !this.options.proxyDomains.includes(proxyDomain)) { - return undefined - } - - // Must be authenticated to use the proxy. - route.provider.ensureAuthenticated(request) - - return { - proxy: { - port, - }, - } +export const authenticated = (req: express.Request): boolean => { + switch (req.args.auth) { + case AuthType.None: + return true + case AuthType.Password: + // The password is stored in the cookie after being hashed. + return req.args.password && req.cookies.key && safeCompare(req.cookies.key, hash(req.args.password)) + default: + throw new Error(`Unsupported auth type ${req.args.auth}`) } } + +/** + * Get the relative path that will get us to the root of the page. For each + * slash we need to go up a directory. For example: + * / => . + * /foo => . + * /foo/ => ./.. + * /foo/bar => ./.. + * /foo/bar/ => ./../.. + */ +export const relativeRoot = (req: express.Request): string => { + const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length + return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) +} + +/** + * Redirect relatively to `/${to}`. Query variables will be preserved. + * `override` will merge with the existing query (use `undefined` to unset). + */ +export const redirect = ( + req: express.Request, + res: express.Response, + to: string, + override: expressCore.Query = {}, +): void => { + const query = Object.assign({}, req.query, override) + Object.keys(override).forEach((key) => { + if (typeof override[key] === "undefined") { + delete query[key] + } + }) + + const relativePath = normalize(`${relativeRoot(req)}/${to}`, true) + const queryString = qs.stringify(query) + const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}` + logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`) + res.redirect(redirectPath) +} + +/** + * Get the value that should be used for setting a cookie domain. This will + * allow the user to authenticate only once. This will use the highest level + * domain (e.g. `coder.com` over `test.coder.com` if both are specified). + */ +export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => { + const idx = host.lastIndexOf(":") + host = idx !== -1 ? host.substring(0, idx) : host + if ( + // Might be blank/missing, so there's nothing more to do. + !host || + // IP addresses can't have subdomains so there's no value in setting the + // domain for them. Assume anything with a : is ipv6 (valid domain name + // characters are alphanumeric or dashes). + host.includes(":") || + // Assume anything entirely numbers and dots is ipv4 (currently tlds + // cannot be entirely numbers). + !/[^0-9.]/.test(host) || + // localhost subdomains don't seem to work at all (browser bug?). + host.endsWith(".localhost") || + // It might be localhost (or an IP, see above) if it's a proxy and it + // isn't setting the host header to match the access domain. + host === "localhost" + ) { + logger.debug("no valid cookie doman", field("host", host)) + return undefined + } + + proxyDomains.forEach((domain) => { + if (host.endsWith(domain) && domain.length < host.length) { + host = domain + } + }) + + logger.debug("got cookie doman", field("host", host)) + return host ? `Domain=${host}` : undefined +} + +declare module "express" { + function Router(options?: express.RouterOptions): express.Router & WithWebsocketMethod + + type WebsocketRequestHandler = ( + socket: net.Socket, + head: Buffer, + req: express.Request, + next: express.NextFunction, + ) => void | Promise + + type WebsocketMethod = (route: expressCore.PathParams, ...handlers: WebsocketRequestHandler[]) => T + + interface WithWebsocketMethod { + ws: WebsocketMethod + } +} + +export const handleUpgrade = (app: express.Express, server: http.Server): void => { + server.on("upgrade", (req, socket, head) => { + socket.on("error", () => socket.destroy()) + + req.ws = socket + req.head = head + req._ws_handled = false + + const res = new http.ServerResponse(req) + res.writeHead = function writeHead(statusCode: number) { + if (statusCode > 200) { + socket.destroy(new Error(`${statusCode}`)) + } + return res + } + + // Send the request off to be handled by Express. + ;(app as any).handle(req, res, () => { + if (!req._ws_handled) { + socket.destroy(new Error("Not found")) + } + }) + }) +} + +/** + * Patch Express routers to handle web sockets and async routes (since we have + * to patch `get` anyway). + * + * Not using express-ws since the ws-wrapped sockets don't work with the proxy + * and wildcards don't work correctly. + */ +function patchRouter(): void { + // Apparently this all works because Router is also the prototype assigned to + // the routers it returns. + + // Store these since the original methods will be overridden. + const originalGet = (express.Router as any).get + const originalPost = (express.Router as any).post + + // Inject the `ws` method. + ;(express.Router as any).ws = function ws( + route: expressCore.PathParams, + ...handlers: express.WebsocketRequestHandler[] + ) { + originalGet.apply(this, [ + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, _, next) => { + if ((req as any).ws) { + ;(req as any)._ws_handled = true + Promise.resolve(handler((req as any).ws, (req as any).head, req, next)).catch(next) + } else { + next() + } + } + return wrapped + }), + ]) + return this + } + // Overwrite `get` so we can distinguish between websocket and non-websocket + // routes. While we're at it handle async responses. + ;(express.Router as any).get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { + originalGet.apply(this, [ + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + if (!(req as any).ws) { + Promise.resolve(handler(req, res, next)).catch(next) + } else { + next() + } + } + return wrapped + }), + ]) + return this + } + // Handle async responses for `post` as well since we're in here anyway. + ;(express.Router as any).post = function post(route: expressCore.PathParams, ...handlers: express.Handler[]) { + originalPost.apply(this, [ + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + Promise.resolve(handler(req, res, next)).catch(next) + } + return wrapped + }), + ]) + return this + } +} + +// This needs to happen before anything uses the router. +patchRouter() diff --git a/src/node/proxy.ts b/src/node/proxy.ts new file mode 100644 index 000000000..8f5dd684b --- /dev/null +++ b/src/node/proxy.ts @@ -0,0 +1,73 @@ +import { Request, Router } from "express" +import proxyServer from "http-proxy" +import { HttpCode } from "../common/http" +import { ensureAuthenticated } from "./http" + +export const proxy = proxyServer.createProxyServer({}) +proxy.on("error", (error, _, res) => { + res.writeHead(HttpCode.ServerError) + res.end(error.message) +}) + +// Intercept the response to rewrite absolute redirects against the base path. +proxy.on("proxyRes", (res, req) => { + if (res.headers.location && res.headers.location.startsWith("/") && (req as any).base) { + res.headers.location = (req as any).base + res.headers.location + } +}) + +export const router = Router() + +/** + * Return the port if the request should be proxied. Anything that ends in a + * proxy domain and has a *single* subdomain should be proxied. Anything else + * should return `undefined` and will be handled as normal. + * + * For example if `coder.com` is specified `8080.coder.com` will be proxied + * but `8080.test.coder.com` and `test.8080.coder.com` will not. + * + * Throw an error if proxying but the user isn't authenticated. + */ +const maybeProxy = (req: Request): string | undefined => { + // Split into parts. + const host = req.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host + const parts = domain.split(".") + + // There must be an exact match. + const port = parts.shift() + const proxyDomain = parts.join(".") + if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + return undefined + } + + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + + return port +} + +router.all("*", (req, res, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + proxy.web(req, res, { + ignorePath: true, + target: `http://127.0.0.1:${port}${req.originalUrl}`, + }) +}) + +router.ws("*", (socket, head, req, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + proxy.ws(req, socket, head, { + ignorePath: true, + target: `http://127.0.0.1:${port}${req.originalUrl}`, + }) +}) diff --git a/src/node/routes/health.ts b/src/node/routes/health.ts index 4e505f57e..20dab71a5 100644 --- a/src/node/routes/health.ts +++ b/src/node/routes/health.ts @@ -1,22 +1,10 @@ -import { Heart } from "../heart" -import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http" +import { Router } from "express" -/** - * Check the heartbeat. - */ -export class HealthHttpProvider extends HttpProvider { - public constructor(options: HttpProviderOptions, private readonly heart: Heart) { - super(options) - } +export const router = Router() - public async handleRequest(): Promise { - return { - cache: false, - mime: "application/json", - content: { - status: this.heart.alive() ? "alive" : "expired", - lastHeartbeat: this.heart.lastHeartbeat, - }, - } - } -} +router.get("/", (req, res) => { + res.json({ + status: req.heart.alive() ? "alive" : "expired", + lastHeartbeat: req.heart.lastHeartbeat, + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts new file mode 100644 index 000000000..82a68866b --- /dev/null +++ b/src/node/routes/index.ts @@ -0,0 +1,123 @@ +import { logger } from "@coder/logger" +import bodyParser from "body-parser" +import cookieParser from "cookie-parser" +import { Express } from "express" +import { promises as fs } from "fs" +import http from "http" +import * as path from "path" +import * as tls from "tls" +import { HttpCode, HttpError } from "../../common/http" +import { plural } from "../../common/util" +import { AuthType, DefaultedArgs } from "../cli" +import { rootPath } from "../constants" +import { Heart } from "../heart" +import { replaceTemplates } from "../http" +import { loadPlugins } from "../plugin" +import * as domainProxy from "../proxy" +import { getMediaMime, paths } from "../util" +import * as health from "./health" +import * as login from "./login" +import * as proxy from "./proxy" +// static is a reserved keyword. +import * as _static from "./static" +import * as update from "./update" +import * as vscode from "./vscode" + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + export interface Request { + args: DefaultedArgs + heart: Heart + } + } +} + +/** + * Register all routes and middleware. + */ +export const register = async (app: Express, server: http.Server, args: DefaultedArgs): Promise => { + const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { + return new Promise((resolve, reject) => { + server.getConnections((error, count) => { + if (error) { + return reject(error) + } + logger.trace(plural(count, `${count} active connection`)) + resolve(count > 0) + }) + }) + }) + + app.disable("x-powered-by") + + app.use(cookieParser()) + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + + server.on("upgrade", () => { + heart.beat() + }) + + app.use(async (req, res, next) => { + heart.beat() + + // If we're handling TLS ensure all requests are redirected to HTTPS. + // TODO: This does *NOT* work if you have a base path since to specify the + // protocol we need to specify the whole path. + if (args.cert && !(req.connection as tls.TLSSocket).encrypted) { + return res.redirect(`https://${req.headers.host}${req.originalUrl}`) + } + + // Return robots.txt. + if (req.originalUrl === "/robots.txt") { + const resourcePath = path.resolve(rootPath, "src/browser/robots.txt") + res.set("Content-Type", getMediaMime(resourcePath)) + return res.send(await fs.readFile(resourcePath)) + } + + // Add common variables routes can use. + req.args = args + req.heart = heart + + return next() + }) + + app.use("/", domainProxy.router) + app.use("/", vscode.router) + app.use("/healthz", health.router) + if (args.auth === AuthType.Password) { + app.use("/login", login.router) + } + app.use("/proxy", proxy.router) + app.use("/static", _static.router) + app.use("/update", update.router) + app.use("/vscode", vscode.router) + + await loadPlugins(app, args) + + app.use(() => { + throw new HttpError("Not Found", HttpCode.NotFound) + }) + + // Handle errors. + // TODO: The types are broken; says they're all implicitly `any`. + app.use(async (err: any, req: any, res: any, next: any) => { + const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") + res.set("Content-Type", getMediaMime(resourcePath)) + try { + const content = await fs.readFile(resourcePath, "utf8") + if (err.code === "ENOENT" || err.code === "EISDIR") { + err.status = HttpCode.NotFound + } + res.status(err.status || 500).send( + replaceTemplates(req, content) + .replace(/{{ERROR_TITLE}}/g, err.status || "Error") + .replace(/{{ERROR_HEADER}}/g, err.status || "Error") + .replace(/{{ERROR_BODY}}/g, err.message), + ) + } catch (error) { + next(error) + } + }) +} diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index 58bd55e0a..bf1058e5e 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -1,140 +1,21 @@ -import * as http from "http" -import * as limiter from "limiter" -import * as querystring from "querystring" -import { HttpCode, HttpError } from "../../common/http" -import { AuthType } from "../cli" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" +import { Router, Request } from "express" +import { promises as fs } from "fs" +import { RateLimiter as Limiter } from "limiter" +import * as path from "path" +import safeCompare from "safe-compare" +import { rootPath } from "../constants" +import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http" import { hash, humanPath } from "../util" -interface LoginPayload { - password?: string - /** - * Since we must set a cookie with an absolute path, we need to know the full - * base path. - */ - base?: string -} - -/** - * Login HTTP provider. - */ -export class LoginHttpProvider extends HttpProvider { - public constructor( - options: HttpProviderOptions, - private readonly configFile: string, - private readonly envPassword: boolean, - ) { - super(options) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (this.options.auth !== AuthType.Password || !this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } - switch (route.base) { - case "/": - switch (request.method) { - case "POST": - this.ensureMethod(request, ["GET", "POST"]) - return this.tryLogin(route, request) - default: - this.ensureMethod(request) - if (this.authenticated(request)) { - return { - redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/", - query: { to: undefined }, - } - } - return this.getRoot(route) - } - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - public async getRoot(route: Route, error?: Error): Promise { - const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html") - response.content = response.content.replace(/{{ERROR}}/, error ? `
${error.message}
` : "") - let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.` - if (this.envPassword) { - passwordMsg = "Password was set from $PASSWORD." - } - response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg) - return this.replaceTemplates(route, response) - } - - private readonly limiter = new RateLimiter() - - /** - * Try logging in. On failure, show the login page with an error. - */ - private async tryLogin(route: Route, request: http.IncomingMessage): Promise { - // Already authenticated via cookies? - const providedPassword = this.authenticated(request) - if (providedPassword) { - return { code: HttpCode.Ok } - } - - try { - if (!this.limiter.try()) { - throw new Error("Login rate limited!") - } - - const data = await this.getData(request) - const payload = data ? querystring.parse(data) : {} - return await this.login(payload, route, request) - } catch (error) { - return this.getRoot(route, error) - } - } - - /** - * Return a cookie if the user is authenticated otherwise throw an error. - */ - private async login(payload: LoginPayload, route: Route, request: http.IncomingMessage): Promise { - const password = this.authenticated(request, { - key: typeof payload.password === "string" ? [hash(payload.password)] : undefined, - }) - - if (password) { - return { - redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/", - query: { to: undefined }, - cookie: - typeof password === "string" - ? { - key: "key", - value: password, - path: payload.base, - } - : undefined, - } - } - - // Only log if it was an actual login attempt. - if (payload && payload.password) { - console.error( - "Failed login attempt", - JSON.stringify({ - xForwardedFor: request.headers["x-forwarded-for"], - remoteAddress: request.connection.remoteAddress, - userAgent: request.headers["user-agent"], - timestamp: Math.floor(new Date().getTime() / 1000), - }), - ) - - throw new Error("Incorrect password") - } - - throw new Error("Missing password") - } +enum Cookie { + Key = "key", } // RateLimiter wraps around the limiter library for logins. // It allows 2 logins every minute and 12 logins every hour. class RateLimiter { - private readonly minuteLimiter = new limiter.RateLimiter(2, "minute") - private readonly hourLimiter = new limiter.RateLimiter(12, "hour") + private readonly minuteLimiter = new Limiter(2, "minute") + private readonly hourLimiter = new Limiter(12, "hour") public try(): boolean { if (this.minuteLimiter.tryRemoveTokens(1)) { @@ -143,3 +24,72 @@ class RateLimiter { return this.hourLimiter.tryRemoveTokens(1) } } + +const getRoot = async (req: Request, error?: Error): Promise => { + const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8") + let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.` + if (req.args.usingEnvPassword) { + passwordMsg = "Password was set from $PASSWORD." + } + return replaceTemplates( + req, + content + .replace(/{{PASSWORD_MSG}}/g, passwordMsg) + .replace(/{{ERROR}}/, error ? `
${error.message}
` : ""), + ) +} + +const limiter = new RateLimiter() + +export const router = Router() + +router.use((req, res, next) => { + const to = (typeof req.query.to === "string" && req.query.to) || "/" + if (authenticated(req)) { + return redirect(req, res, to, { to: undefined }) + } + next() +}) + +router.get("/", async (req, res) => { + res.send(await getRoot(req)) +}) + +router.post("/", async (req, res) => { + try { + if (!limiter.try()) { + throw new Error("Login rate limited!") + } + + if (!req.body.password) { + throw new Error("Missing password") + } + + if (req.args.password && safeCompare(req.body.password, req.args.password)) { + // The hash does not add any actual security but we do it for + // obfuscation purposes (and as a side effect it handles escaping). + res.cookie(Cookie.Key, hash(req.body.password), { + domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]), + path: req.body.base || "/", + sameSite: "lax", + }) + + const to = (typeof req.query.to === "string" && req.query.to) || "/" + return redirect(req, res, to, { to: undefined }) + } + + console.error( + "Failed login attempt", + JSON.stringify({ + xForwardedFor: req.headers["x-forwarded-for"], + remoteAddress: req.connection.remoteAddress, + userAgent: req.headers["user-agent"], + timestamp: Math.floor(new Date().getTime() / 1000), + }), + ) + + throw new Error("Incorrect password") + } catch (error) { + res.send(await getRoot(req, error)) + } +}) diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index a332cc055..8c83827a6 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -1,43 +1,43 @@ -import * as http from "http" +import { Request, Router } from "express" +import qs from "qs" import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpResponse, Route, WsResponse } from "../http" +import { authenticated, redirect } from "../http" +import { proxy } from "../proxy" -/** - * Proxy HTTP provider. - */ -export class ProxyHttpProvider extends HttpProvider { - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (!this.authenticated(request)) { - if (this.isRoot(route)) { - return { redirect: "/login", query: { to: route.fullPath } } - } - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } +export const router = Router() - // Ensure there is a trailing slash so relative paths work correctly. - if (this.isRoot(route) && !route.fullPath.endsWith("/")) { - return { - redirect: `${route.fullPath}/`, - } - } - - const port = route.base.replace(/^\//, "") - return { - proxy: { - strip: `${route.providerBase}/${port}`, - port, - }, - } - } - - public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise { - this.ensureAuthenticated(request) - const port = route.base.replace(/^\//, "") - return { - proxy: { - strip: `${route.providerBase}/${port}`, - port, - }, - } +const getProxyTarget = (req: Request, rewrite: boolean): string => { + if (rewrite) { + const query = qs.stringify(req.query) + return `http://127.0.0.1:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` } + return `http://127.0.0.1:${req.params.port}/${req.originalUrl}` } + +router.all("/(:port)(/*)?", (req, res) => { + if (!authenticated(req)) { + // If visiting the root (/proxy/:port and nothing else) redirect to the + // login page. + if (!req.params[0] || req.params[0] === "/") { + return redirect(req, res, "login", { + to: `${req.baseUrl}${req.path}` || "/", + }) + } + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + // Absolute redirects need to be based on the subpath when rewriting. + ;(req as any).base = `${req.baseUrl}/${req.params.port}` + + proxy.web(req, res, { + ignorePath: true, + target: getProxyTarget(req, true), + }) +}) + +router.ws("/(:port)(/*)?", (socket, head, req) => { + proxy.ws(req, socket, head, { + ignorePath: true, + target: getProxyTarget(req, true), + }) +}) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts index 471d0c987..0678c2313 100644 --- a/src/node/routes/static.ts +++ b/src/node/routes/static.ts @@ -1,73 +1,61 @@ import { field, logger } from "@coder/logger" -import * as http from "http" +import { Router } from "express" +import { promises as fs } from "fs" import * as path from "path" import { Readable } from "stream" import * as tarFs from "tar-fs" import * as zlib from "zlib" -import { HttpProvider, HttpResponse, Route } from "../http" -import { pathToFsPath } from "../util" +import { HttpCode, HttpError } from "../../common/http" +import { rootPath } from "../constants" +import { authenticated, replaceTemplates } from "../http" +import { getMediaMime, pathToFsPath } from "../util" -/** - * Static file HTTP provider. Static requests do not require authentication if - * the resource is in the application's directory except requests to serve a - * directory as a tar which always requires authentication. - */ -export class StaticHttpProvider extends HttpProvider { - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureMethod(request) +export const router = Router() - if (typeof route.query.tar === "string") { - this.ensureAuthenticated(request) - return this.getTarredResource(request, pathToFsPath(route.query.tar)) - } - - const response = await this.getReplacedResource(request, route) - if (!this.isDev) { - response.cache = true - } - return response +// The commit is for caching. +router.get("/(:commit)(/*)?", async (req, res) => { + if (!req.params[0]) { + throw new HttpError("Not Found", HttpCode.NotFound) } - /** - * Return a resource with variables replaced where necessary. - */ - protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise { - // The first part is always the commit (for caching purposes). - const split = route.requestPath.split("/").slice(1) + const resourcePath = path.resolve(req.params[0]) - const resourcePath = path.resolve("/", ...split) - - // Make sure it's in code-server or a plugin. - const validPaths = [this.rootPath, process.env.PLUGIN_DIR] - if (!validPaths.find((p) => p && resourcePath.startsWith(p))) { - this.ensureAuthenticated(request) - } - - switch (split[split.length - 1]) { - case "manifest.json": { - const response = await this.getUtf8Resource(resourcePath) - return this.replaceTemplates(route, response) - } - } - return this.getResource(resourcePath) + // Make sure it's in code-server if you aren't authenticated. This lets + // unauthenticated users load the login assets. + if (!resourcePath.startsWith(rootPath) && !authenticated(req)) { + throw new HttpError("Unauthorized", HttpCode.Unauthorized) } - /** - * Tar up and stream a directory. - */ - private async getTarredResource(request: http.IncomingMessage, ...parts: string[]): Promise { - const filePath = path.join(...parts) - let stream: Readable = tarFs.pack(filePath) - const headers: http.OutgoingHttpHeaders = {} - if (request.headers["accept-encoding"] && request.headers["accept-encoding"].includes("gzip")) { - logger.debug("gzipping tar", field("filePath", filePath)) + // Don't cache during development. - can also be used if you want to make a + // static request without caching. + if (req.params.commit !== "development" && req.params.commit !== "-") { + res.header("Cache-Control", "public, max-age=31536000") + } + + const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar + if (typeof tar === "string") { + let stream: Readable = tarFs.pack(pathToFsPath(tar)) + if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) { + logger.debug("gzipping tar", field("path", resourcePath)) const compress = zlib.createGzip() stream.pipe(compress) stream.on("error", (error) => compress.destroy(error)) stream.on("close", () => compress.end()) stream = compress - headers["content-encoding"] = "gzip" + res.header("content-encoding", "gzip") } - return { stream, filePath, mime: "application/x-tar", cache: true, headers } + res.set("Content-Type", "application/x-tar") + stream.on("close", () => res.end()) + return stream.pipe(res) } -} + + res.set("Content-Type", getMediaMime(resourcePath)) + + if (resourcePath.endsWith("manifest.json")) { + const content = await fs.readFile(resourcePath, "utf8") + return res.send(replaceTemplates(req, content)) + } + + const content = await fs.readFile(resourcePath) + return res.send(content) +}) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index a83f578e1..ea7479fe0 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -1,172 +1,34 @@ -import { field, logger } from "@coder/logger" -import * as http from "http" -import * as https from "https" -import * as path from "path" -import * as semver from "semver" -import * as url from "url" -import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings" +import { Router } from "express" +import { version } from "../constants" +import { ensureAuthenticated } from "../http" +import { UpdateProvider } from "../update" -export interface Update { - checked: number - version: string -} +export const router = Router() -export interface LatestResponse { - name: string -} +const provider = new UpdateProvider() -/** - * HTTP provider for checking updates (does not download/install them). - */ -export class UpdateHttpProvider extends HttpProvider { - private update?: Promise - private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks. +router.use((req, _, next) => { + ensureAuthenticated(req) + next() +}) - public constructor( - options: HttpProviderOptions, - public readonly enabled: boolean, - /** - * The URL for getting the latest version of code-server. Should return JSON - * that fulfills `LatestResponse`. - */ - private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest", - /** - * Update information will be stored here. If not provided, the global - * settings will be used. - */ - private readonly settings: SettingsProvider = globalSettings, - ) { - super(options) - } +router.get("/", async (_, res) => { + const update = await provider.getUpdate() + res.json({ + checked: update.checked, + latest: update.version, + current: version, + isLatest: provider.isLatestVersion(update), + }) +}) - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureAuthenticated(request) - this.ensureMethod(request) - - if (!this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } - - if (!this.enabled) { - throw new Error("update checks are disabled") - } - - switch (route.base) { - case "/check": - case "/": { - const update = await this.getUpdate(route.base === "/check") - return { - content: { - ...update, - isLatest: this.isLatestVersion(update), - }, - } - } - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - /** - * Query for and return the latest update. - */ - public async getUpdate(force?: boolean): Promise { - // Don't run multiple requests at a time. - if (!this.update) { - this.update = this._getUpdate(force) - this.update.then(() => (this.update = undefined)) - } - - return this.update - } - - private async _getUpdate(force?: boolean): Promise { - const now = Date.now() - try { - let { update } = !force ? await this.settings.read() : { update: undefined } - if (!update || update.checked + this.updateInterval < now) { - const buffer = await this.request(this.latestUrl) - const data = JSON.parse(buffer.toString()) as LatestResponse - update = { checked: now, version: data.name } - await this.settings.write({ update }) - } - logger.debug("got latest version", field("latest", update.version)) - return update - } catch (error) { - logger.error("Failed to get latest version", field("error", error.message)) - return { - checked: now, - version: "unknown", - } - } - } - - public get currentVersion(): string { - return require(path.resolve(__dirname, "../../../package.json")).version - } - - /** - * Return true if the currently installed version is the latest. - */ - public isLatestVersion(latest: Update): boolean { - const version = this.currentVersion - logger.debug("comparing versions", field("current", version), field("latest", latest.version)) - try { - return latest.version === version || semver.lt(latest.version, version) - } catch (error) { - return true - } - } - - private async request(uri: string): Promise { - const response = await this.requestResponse(uri) - return new Promise((resolve, reject) => { - const chunks: Buffer[] = [] - let bufferLength = 0 - response.on("data", (chunk) => { - bufferLength += chunk.length - chunks.push(chunk) - }) - response.on("error", reject) - response.on("end", () => { - resolve(Buffer.concat(chunks, bufferLength)) - }) - }) - } - - private async requestResponse(uri: string): Promise { - let redirects = 0 - const maxRedirects = 10 - return new Promise((resolve, reject) => { - const request = (uri: string): void => { - logger.debug("Making request", field("uri", uri)) - const httpx = uri.startsWith("https") ? https : http - const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { - if ( - response.statusCode && - response.statusCode >= 300 && - response.statusCode < 400 && - response.headers.location - ) { - ++redirects - if (redirects > maxRedirects) { - return reject(new Error("reached max redirects")) - } - response.destroy() - return request(url.resolve(uri, response.headers.location)) - } - - if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { - return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) - } - - resolve(response) - }) - client.on("error", reject) - } - request(uri) - }) - } -} +// This route will force a check. +router.get("/check", async (_, res) => { + const update = await provider.getUpdate(true) + res.json({ + checked: update.checked, + latest: update.version, + current: version, + isLatest: provider.isLatestVersion(update), + }) +}) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index ed4f714e5..5f8dd7584 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -1,237 +1,99 @@ -import { field, logger } from "@coder/logger" -import * as cp from "child_process" import * as crypto from "crypto" -import * as fs from "fs-extra" -import * as http from "http" -import * as net from "net" +import { Router } from "express" +import { promises as fs } from "fs" import * as path from "path" -import { - CodeServerMessage, - Options, - StartPath, - VscodeMessage, - VscodeOptions, - WorkbenchOptions, -} from "../../../lib/vscode/src/vs/server/ipc" -import { HttpCode, HttpError } from "../../common/http" -import { arrayify, generateUuid } from "../../common/util" -import { Args } from "../cli" -import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" -import { settings } from "../settings" -import { pathToFsPath } from "../util" +import { commit, rootPath, version } from "../constants" +import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" +import { getMediaMime, pathToFsPath } from "../util" +import { VscodeProvider } from "../vscode" -export class VscodeHttpProvider extends HttpProvider { - private readonly serverRootPath: string - private readonly vsRootPath: string - private _vscode?: Promise +export const router = Router() - public constructor(options: HttpProviderOptions, private readonly args: Args) { - super(options) - this.vsRootPath = path.resolve(this.rootPath, "lib/vscode") - this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") - } +const vscode = new VscodeProvider() - public get running(): boolean { - return !!this._vscode - } - - public async dispose(): Promise { - if (this._vscode) { - const vscode = await this._vscode - vscode.removeAllListeners() - this._vscode = undefined - vscode.kill() - } - } - - private async initialize(options: VscodeOptions): Promise { - const id = generateUuid() - const vscode = await this.fork() - - logger.debug("setting up vs code...") - return new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "options" && message.id === id - ? resolve(message.options) - : reject(new Error("Unexpected response during initialization")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - this.send({ type: "init", id, options }, vscode) +router.get("/", async (req, res) => { + if (!authenticated(req)) { + return redirect(req, res, "login", { + to: req.baseUrl || "/", }) } - private fork(): Promise { - if (!this._vscode) { - logger.debug("forking vs code...") - const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) - this._vscode = undefined - }) - vscode.on("exit", (code) => { - logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined - }) - - this._vscode = new Promise((resolve, reject) => { - vscode.once("message", (message: VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - }) - } - - return this._vscode - } - - public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise { - if (!this.authenticated(request)) { - throw new Error("not authenticated") - } - - // VS Code expects a raw socket. It will handle all the web socket frames. - // We just need to handle the initial upgrade. - // 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 vscode = await this._vscode - this.send({ type: "socket", query: route.query }, vscode, socket) - } - - private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { - if (!vscode || vscode.killed) { - throw new Error("vscode is not running") - } - vscode.send(message, socket) - } - - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - this.ensureMethod(request) - - switch (route.base) { - case "/": - if (!this.isRoot(route)) { - throw new HttpError("Not found", HttpCode.NotFound) - } else if (!this.authenticated(request)) { - return { redirect: "/login", query: { to: route.providerBase } } - } - try { - return await this.getRoot(request, route) - } catch (error) { - const message = `
VS Code failed to load.
${ - this.isDev - ? `
It might not have finished compiling.
` + - `Check for Finished compilation in the output.` - : "" - }

${error}` - return this.getErrorRoot(route, "VS Code failed to load", "500", message) - } - } - - this.ensureAuthenticated(request) - - switch (route.base) { - case "/resource": - case "/vscode-remote-resource": - if (typeof route.query.path === "string") { - return this.getResource(pathToFsPath(route.query.path)) - } - break - case "/webview": - if (/^\/vscode-resource/.test(route.requestPath)) { - return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, "")) - } - return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath) - } - - throw new HttpError("Not found", HttpCode.NotFound) - } - - private async getRoot(request: http.IncomingMessage, route: Route): Promise { - const remoteAuthority = request.headers.host as string - const { lastVisited } = await settings.read() - const startPath = await this.getFirstPath([ - { url: route.query.workspace, workspace: true }, - { url: route.query.folder, workspace: false }, - this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined, - lastVisited, - ]) - const [response, options] = await Promise.all([ - await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"), - this.initialize({ - args: this.args, - remoteAuthority, - startPath, + const [content, options] = await Promise.all([ + await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"), + vscode + .initialize( + { + args: req.args, + remoteAuthority: req.headers.host || "", + }, + req.query, + ) + .catch((error) => { + const devMessage = commit === "development" ? "It might not have finished compiling." : "" + throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) }), - ]) + ]) - settings.write({ - lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited - query: route.query, - }) + options.productConfiguration.codeServerVersion = version - if (!this.isDev) { - response.content = response.content.replace(//g, "") - } - - options.productConfiguration.codeServerVersion = require("../../../package.json").version - - response.content = response.content + res.send( + replaceTemplates( + 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, + { + disableTelemetry: !!req.args["disable-telemetry"], + }, + ) .replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`) .replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`) .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) - .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`) - return this.replaceTemplates(route, response, { - disableTelemetry: !!this.args["disable-telemetry"], - }) - } + .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`), + ) +}) - /** - * Choose the first non-empty path. - */ - private async getFirstPath( - startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>, - ): Promise { - const isFile = async (path: string): Promise => { - try { - const stat = await fs.stat(path) - return stat.isFile() - } catch (error) { - logger.warn(error.message) - return false - } - } - for (let i = 0; i < startPaths.length; ++i) { - const startPath = startPaths[i] - const url = arrayify(startPath && startPath.url).find((p) => !!p) - if (startPath && url) { - return { - url, - // The only time `workspace` is undefined is for the command-line - // argument, in which case it's a path (not a URL) so we can stat it - // without having to parse it. - workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), - } - } - } - return undefined +router.ws("/", async (socket, _, req) => { + ensureAuthenticated(req) + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + const reply = crypto + .createHash("sha1") + .update(req.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", + ) + await vscode.sendWebsocket(socket, req.query) +}) + +router.get("/resource(/*)?", async (req, res) => { + ensureAuthenticated(req) + if (typeof req.query.path === "string") { + res.set("Content-Type", getMediaMime(req.query.path)) + res.send(await fs.readFile(pathToFsPath(req.query.path))) } -} +}) + +router.get("/vscode-remote-resource(/*)?", async (req, res) => { + ensureAuthenticated(req) + if (typeof req.query.path === "string") { + res.set("Content-Type", getMediaMime(req.query.path)) + res.send(await fs.readFile(pathToFsPath(req.query.path))) + } +}) + +router.get("/webview/*", async (req, res) => { + ensureAuthenticated(req) + res.set("Content-Type", getMediaMime(req.path)) + if (/^\/vscode-resource/.test(req.path)) { + return res.send(await fs.readFile(req.path.replace(/^\/vscode-resource(\/file)?/, ""))) + } + return res.send( + await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), + ) +}) diff --git a/src/node/settings.ts b/src/node/settings.ts index d68e8e3bd..5f9427aa1 100644 --- a/src/node/settings.ts +++ b/src/node/settings.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" +import { Query } from "express-serve-static-core" import * as fs from "fs-extra" import * as path from "path" -import { Route } from "./http" import { paths } from "./util" export type Settings = { [key: string]: Settings | string | boolean | number } @@ -58,7 +58,7 @@ export interface CoderSettings extends UpdateSettings { url: string workspace: boolean } - query: Route["query"] + query: Query } /** diff --git a/src/node/update.ts b/src/node/update.ts new file mode 100644 index 000000000..42baa1848 --- /dev/null +++ b/src/node/update.ts @@ -0,0 +1,133 @@ +import { field, logger } from "@coder/logger" +import * as http from "http" +import * as https from "https" +import * as semver from "semver" +import * as url from "url" +import { version } from "./constants" +import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings" + +export interface Update { + checked: number + version: string +} + +export interface LatestResponse { + name: string +} + +/** + * Provide update information. + */ +export class UpdateProvider { + private update?: Promise + private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks. + + public constructor( + /** + * The URL for getting the latest version of code-server. Should return JSON + * that fulfills `LatestResponse`. + */ + private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest", + /** + * Update information will be stored here. If not provided, the global + * settings will be used. + */ + private readonly settings: SettingsProvider = globalSettings, + ) {} + + /** + * Query for and return the latest update. + */ + public async getUpdate(force?: boolean): Promise { + // Don't run multiple requests at a time. + if (!this.update) { + this.update = this._getUpdate(force) + this.update.then(() => (this.update = undefined)) + } + + return this.update + } + + private async _getUpdate(force?: boolean): Promise { + const now = Date.now() + try { + let { update } = !force ? await this.settings.read() : { update: undefined } + if (!update || update.checked + this.updateInterval < now) { + const buffer = await this.request(this.latestUrl) + const data = JSON.parse(buffer.toString()) as LatestResponse + update = { checked: now, version: data.name.replace(/^v/, "") } + await this.settings.write({ update }) + } + logger.debug("got latest version", field("latest", update.version)) + return update + } catch (error) { + logger.error("Failed to get latest version", field("error", error.message)) + return { + checked: now, + version: "unknown", + } + } + } + + /** + * Return true if the currently installed version is the latest. + */ + public isLatestVersion(latest: Update): boolean { + logger.debug("comparing versions", field("current", version), field("latest", latest.version)) + try { + return latest.version === version || semver.lt(latest.version, version) + } catch (error) { + return true + } + } + + private async request(uri: string): Promise { + const response = await this.requestResponse(uri) + return new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + let bufferLength = 0 + response.on("data", (chunk) => { + bufferLength += chunk.length + chunks.push(chunk) + }) + response.on("error", reject) + response.on("end", () => { + resolve(Buffer.concat(chunks, bufferLength)) + }) + }) + } + + private async requestResponse(uri: string): Promise { + let redirects = 0 + const maxRedirects = 10 + return new Promise((resolve, reject) => { + const request = (uri: string): void => { + logger.debug("Making request", field("uri", uri)) + const httpx = uri.startsWith("https") ? https : http + const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { + if ( + response.statusCode && + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + ++redirects + if (redirects > maxRedirects) { + return reject(new Error("reached max redirects")) + } + response.destroy() + return request(url.resolve(uri, response.headers.location)) + } + + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { + return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) + } + + resolve(response) + }) + client.on("error", reject) + } + request(uri) + }) + } +} diff --git a/src/node/vscode.ts b/src/node/vscode.ts new file mode 100644 index 000000000..5cf4e4b76 --- /dev/null +++ b/src/node/vscode.ts @@ -0,0 +1,150 @@ +import { field, logger } from "@coder/logger" +import * as cp from "child_process" +import * as fs from "fs-extra" +import * as net from "net" +import * as path from "path" +import * as ipc from "../../lib/vscode/src/vs/server/ipc" +import { arrayify, generateUuid } from "../common/util" +import { rootPath } from "./constants" +import { settings } from "./settings" + +export class VscodeProvider { + public readonly serverRootPath: string + public readonly vsRootPath: string + private _vscode?: Promise + + public constructor() { + this.vsRootPath = path.resolve(rootPath, "lib/vscode") + this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") + } + + public async dispose(): Promise { + if (this._vscode) { + const vscode = await this._vscode + vscode.removeAllListeners() + this._vscode = undefined + vscode.kill() + } + } + + public async initialize( + options: Omit, + query: ipc.Query, + ): Promise { + const { lastVisited } = await settings.read() + const startPath = await this.getFirstPath([ + { url: query.workspace, workspace: true }, + { url: query.folder, workspace: false }, + options.args._ && options.args._.length > 0 + ? { url: path.resolve(options.args._[options.args._.length - 1]) } + : undefined, + lastVisited, + ]) + + settings.write({ + lastVisited: startPath, + query, + }) + + const id = generateUuid() + const vscode = await this.fork() + + logger.debug("setting up vs code...") + return new Promise((resolve, reject) => { + vscode.once("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vs code", field("message", message)) + return message.type === "options" && message.id === id + ? resolve(message.options) + : reject(new Error("Unexpected response during initialization")) + }) + vscode.once("error", reject) + vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + this.send( + { + type: "init", + id, + options: { + ...options, + startPath, + }, + }, + vscode, + ) + }) + } + + private fork(): Promise { + if (!this._vscode) { + logger.debug("forking vs code...") + const vscode = cp.fork(path.join(this.serverRootPath, "fork")) + vscode.on("error", (error) => { + logger.error(error.message) + this._vscode = undefined + }) + vscode.on("exit", (code) => { + logger.error(`VS Code exited unexpectedly with code ${code}`) + this._vscode = undefined + }) + + this._vscode = new Promise((resolve, reject) => { + vscode.once("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vs code", field("message", message)) + return message.type === "ready" + ? resolve(vscode) + : reject(new Error("Unexpected response waiting for ready response")) + }) + vscode.once("error", reject) + vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + }) + } + + return this._vscode + } + + /** + * VS Code expects a raw socket. It will handle all the web socket frames. + */ + public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise { + // TODO: TLS socket proxy. + const vscode = await this._vscode + this.send({ type: "socket", query }, vscode, socket) + } + + private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { + if (!vscode || vscode.killed) { + throw new Error("vscode is not running") + } + vscode.send(message, socket) + } + + /** + * Choose the first non-empty path. + */ + private async getFirstPath( + startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, + ): Promise { + const isFile = async (path: string): Promise => { + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (error) { + logger.warn(error.message) + return false + } + } + for (let i = 0; i < startPaths.length; ++i) { + const startPath = startPaths[i] + const url = arrayify(startPath && startPath.url).find((p) => !!p) + if (startPath && url && typeof url === "string") { + return { + url, + // The only time `workspace` is undefined is for the command-line + // argument, in which case it's a path (not a URL) so we can stat it + // without having to parse it. + workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), + } + } + } + return undefined + } +} diff --git a/test/update.test.ts b/test/update.test.ts index 093429be3..29c558f56 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -2,9 +2,8 @@ import * as assert from "assert" import * as fs from "fs-extra" import * as http from "http" import * as path from "path" -import { AuthType } from "../src/node/cli" -import { LatestResponse, UpdateHttpProvider } from "../src/node/routes/update" import { SettingsProvider, UpdateSettings } from "../src/node/settings" +import { LatestResponse, UpdateProvider } from "../src/node/update" import { tmpdir } from "../src/node/util" describe.skip("update", () => { @@ -34,22 +33,14 @@ describe.skip("update", () => { const jsonPath = path.join(tmpdir, "tests/updates/update.json") const settings = new SettingsProvider(jsonPath) - let _provider: UpdateHttpProvider | undefined - const provider = (): UpdateHttpProvider => { + let _provider: UpdateProvider | undefined + const provider = (): UpdateProvider => { if (!_provider) { const address = server.address() if (!address || typeof address === "string" || !address.port) { throw new Error("unexpected address") } - _provider = new UpdateHttpProvider( - { - auth: AuthType.None, - commit: "test", - }, - true, - `http://${address.address}:${address.port}/latest`, - settings, - ) + _provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, settings) } return _provider } @@ -153,14 +144,10 @@ describe.skip("update", () => { }) it("should not reject if unable to fetch", async () => { - const options = { - auth: AuthType.None, - commit: "test", - } - let provider = new UpdateHttpProvider(options, true, "invalid", settings) + let provider = new UpdateProvider("invalid", settings) await assert.doesNotReject(() => provider.getUpdate(true)) - provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings) + provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings) await assert.doesNotReject(() => provider.getUpdate(true)) }) }) diff --git a/yarn.lock b/yarn.lock index d1224a32f..1d98caab6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1013,7 +1013,7 @@ traverse "^0.6.6" unified "^6.1.6" -"@types/body-parser@*": +"@types/body-parser@*", "@types/body-parser@^1.19.0": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== @@ -1028,6 +1028,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1042,7 +1049,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.8": +"@types/express@*", "@types/express@^4.17.8": version "4.17.8" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== @@ -1659,7 +1666,7 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== -body-parser@1.19.0: +body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== @@ -2251,6 +2258,14 @@ convert-source-map@^1.5.1, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.5: + version "1.4.5" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" From 257d9a4fa4de1b50af26204a61a7528a3ead25ce Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 26 Oct 2020 15:13:52 -0500 Subject: [PATCH 09/82] Make authentication work with sub-domain proxy --- src/node/proxy.ts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index 8f5dd684b..d6dd795de 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import proxyServer from "http-proxy" -import { HttpCode } from "../common/http" -import { ensureAuthenticated } from "./http" +import { HttpCode, HttpError } from "../common/http" +import { authenticated, ensureAuthenticated } from "./http" export const proxy = proxyServer.createProxyServer({}) proxy.on("error", (error, _, res) => { @@ -42,18 +42,39 @@ const maybeProxy = (req: Request): string | undefined => { return undefined } - // Must be authenticated to use the proxy. - ensureAuthenticated(req) - return port } +/** + * Determine if the user is browsing /, /login, or static assets and if so fall + * through to allow the redirect and login flow. + */ +const shouldFallThrough = (req: Request): boolean => { + // The ideal would be to have a reliable way to detect if this is a request + // for (or originating from) our root or login HTML. But requests for HTML + // don't seem to set any content type. + return ( + req.headers["content-type"] !== "application/json" && + ((req.originalUrl.startsWith("/") && req.method === "GET") || + (req.originalUrl.startsWith("/static") && req.method === "GET") || + (req.originalUrl.startsWith("/login") && (req.method === "GET" || req.method === "POST"))) + ) +} + router.all("*", (req, res, next) => { const port = maybeProxy(req) if (!port) { return next() } + // Must be authenticated to use the proxy. + if (!authenticated(req)) { + if (shouldFallThrough(req)) { + return next() + } + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + proxy.web(req, res, { ignorePath: true, target: `http://127.0.0.1:${port}${req.originalUrl}`, @@ -66,6 +87,9 @@ router.ws("*", (socket, head, req, next) => { return next() } + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + proxy.ws(req, socket, head, { ignorePath: true, target: `http://127.0.0.1:${port}${req.originalUrl}`, From 6422a8d74b0b644189e6589dbb4f8d7945008421 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:17:05 -0500 Subject: [PATCH 10/82] Fix webview resource path --- src/node/routes/vscode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 5f8dd7584..adef232fd 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -90,8 +90,8 @@ router.get("/vscode-remote-resource(/*)?", async (req, res) => { router.get("/webview/*", async (req, res) => { ensureAuthenticated(req) res.set("Content-Type", getMediaMime(req.path)) - if (/^\/vscode-resource/.test(req.path)) { - return res.send(await fs.readFile(req.path.replace(/^\/vscode-resource(\/file)?/, ""))) + if (/^vscode-resource/.test(req.params[0])) { + return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, ""))) } return res.send( await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), From 6ab6cb4f0731ce122f75204c06ef85e6ae1bc4d8 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:18:44 -0500 Subject: [PATCH 11/82] Fix error handler types --- src/node/routes/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 82a68866b..8c541d555 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" import bodyParser from "body-parser" import cookieParser from "cookie-parser" -import { Express } from "express" +import { ErrorRequestHandler, Express } from "express" import { promises as fs } from "fs" import http from "http" import * as path from "path" @@ -100,9 +100,7 @@ export const register = async (app: Express, server: http.Server, args: Defaulte throw new HttpError("Not Found", HttpCode.NotFound) }) - // Handle errors. - // TODO: The types are broken; says they're all implicitly `any`. - app.use(async (err: any, req: any, res: any, next: any) => { + const errorHandler: ErrorRequestHandler = async (err, req, res, next) => { const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") res.set("Content-Type", getMediaMime(resourcePath)) try { @@ -110,14 +108,17 @@ export const register = async (app: Express, server: http.Server, args: Defaulte if (err.code === "ENOENT" || err.code === "EISDIR") { err.status = HttpCode.NotFound } - res.status(err.status || 500).send( + const status = err.status ?? err.statusCode ?? 500 + res.status(status).send( replaceTemplates(req, content) - .replace(/{{ERROR_TITLE}}/g, err.status || "Error") - .replace(/{{ERROR_HEADER}}/g, err.status || "Error") + .replace(/{{ERROR_TITLE}}/g, status) + .replace(/{{ERROR_HEADER}}/g, status) .replace(/{{ERROR_BODY}}/g, err.message), ) } catch (error) { next(error) } - }) + } + + app.use(errorHandler) } From 305348f0aca6e6944943ec807675f7af4b02ab72 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:26:22 -0500 Subject: [PATCH 12/82] Improve proxy fallthrough logic - Use accept header. - Match /login and /login/ exactly. - Match /static/ (trailing slash). - Use req.path. Same result but feels more accurate to me. --- src/node/proxy.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d6dd795de..4c861d0fe 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -50,15 +50,18 @@ const maybeProxy = (req: Request): string | undefined => { * through to allow the redirect and login flow. */ const shouldFallThrough = (req: Request): boolean => { - // The ideal would be to have a reliable way to detect if this is a request - // for (or originating from) our root or login HTML. But requests for HTML - // don't seem to set any content type. - return ( - req.headers["content-type"] !== "application/json" && - ((req.originalUrl.startsWith("/") && req.method === "GET") || - (req.originalUrl.startsWith("/static") && req.method === "GET") || - (req.originalUrl.startsWith("/login") && (req.method === "GET" || req.method === "POST"))) - ) + // See if it looks like a request for the root or login HTML. + if (req.accepts("text/html")) { + if ( + (req.path === "/" && req.method === "GET") || + (/\/login\/?/.test(req.path) && (req.method === "GET" || req.method === "POST")) + ) { + return true + } + } + + // See if it looks like a request for a static asset. + return req.path.startsWith("/static/") && req.method === "GET" } router.all("*", (req, res, next) => { From cde94d5ed4c8e12baa9ff7f2d43ae9a835906095 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:35:42 -0500 Subject: [PATCH 13/82] Remove redundant serverAddress check We now guarantee there is an address. --- src/node/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 30dd93d8f..3b95f9e90 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -159,7 +159,7 @@ const main = async (args: DefaultedArgs): Promise => { } } - if (serverAddress && !args.socket && args.open) { + 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") await open(openAddress).catch((error: Error) => { From dc177ab50580a5df971b0f1fbe539a9259b28139 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:38:54 -0500 Subject: [PATCH 14/82] Unambiguify address replacement Co-authored-by: Teffen Ellis --- src/node/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 3b95f9e90..890ad6ae8 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -161,7 +161,7 @@ const main = async (args: DefaultedArgs): Promise => { 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") + const openAddress = serverAddress.replace("://0.0.0.0", "://localhost") await open(openAddress).catch((error: Error) => { logger.error("Failed to open", field("address", openAddress), field("error", error)) }) From 504d89638b4b55a62ffc65144cce6a140aedcde5 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:41:11 -0500 Subject: [PATCH 15/82] Fix open line being printed when open fails Opening the URL can fail if the user doesn't have something appropriate installed to handle it. --- src/node/entry.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 890ad6ae8..df218a52d 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -162,10 +162,12 @@ const main = async (args: DefaultedArgs): Promise => { 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") - await open(openAddress).catch((error: Error) => { + try { + await open(openAddress) + logger.info(`Opened ${openAddress}`) + } catch (error) { logger.error("Failed to open", field("address", openAddress), field("error", error)) - }) - logger.info(`Opened ${openAddress}`) + } } } From f2f1fee6f179059a1a5ed40e36192946c0d4bc86 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 27 Oct 2020 17:48:37 -0500 Subject: [PATCH 16/82] Short-circuit heartbeat when alive --- src/node/heart.ts | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/node/heart.ts b/src/node/heart.ts index 5198e33d8..eed070e4e 100644 --- a/src/node/heart.ts +++ b/src/node/heart.ts @@ -21,26 +21,28 @@ export class Heart { * activity. Failures are logged as warnings. */ public beat(): void { - if (!this.alive()) { - logger.trace("heartbeat") - fs.writeFile(this.heartbeatPath, "").catch((error) => { - logger.warn(error.message) - }) - this.lastHeartbeat = Date.now() - if (typeof this.heartbeatTimer !== "undefined") { - clearTimeout(this.heartbeatTimer) - } - this.heartbeatTimer = setTimeout(() => { - this.isActive() - .then((active) => { - if (active) { - this.beat() - } - }) - .catch((error) => { - logger.warn(error.message) - }) - }, this.heartbeatInterval) + if (this.alive()) { + return } + + logger.trace("heartbeat") + fs.writeFile(this.heartbeatPath, "").catch((error) => { + logger.warn(error.message) + }) + this.lastHeartbeat = Date.now() + if (typeof this.heartbeatTimer !== "undefined") { + clearTimeout(this.heartbeatTimer) + } + this.heartbeatTimer = setTimeout(() => { + this.isActive() + .then((active) => { + if (active) { + this.beat() + } + }) + .catch((error) => { + logger.warn(error.message) + }) + }, this.heartbeatInterval) } } From 1067507c41810a24eccc02ee7645b762d25b2a0e Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 28 Oct 2020 11:29:43 -0500 Subject: [PATCH 17/82] Proxy to 0.0.0.0 instead of localhost --- src/node/proxy.ts | 4 ++-- src/node/routes/proxy.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index 4c861d0fe..a95badad9 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -80,7 +80,7 @@ router.all("*", (req, res, next) => { proxy.web(req, res, { ignorePath: true, - target: `http://127.0.0.1:${port}${req.originalUrl}`, + target: `http://0.0.0.0:${port}${req.originalUrl}`, }) }) @@ -95,6 +95,6 @@ router.ws("*", (socket, head, req, next) => { proxy.ws(req, socket, head, { ignorePath: true, - target: `http://127.0.0.1:${port}${req.originalUrl}`, + target: `http://0.0.0.0:${port}${req.originalUrl}`, }) }) diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index 8c83827a6..29aa999ae 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -9,9 +9,9 @@ export const router = Router() const getProxyTarget = (req: Request, rewrite: boolean): string => { if (rewrite) { const query = qs.stringify(req.query) - return `http://127.0.0.1:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` + return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}` } - return `http://127.0.0.1:${req.params.port}/${req.originalUrl}` + return `http://0.0.0.0:${req.params.port}/${req.originalUrl}` } router.all("/(:port)(/*)?", (req, res) => { From 8a9e61defbe45eebc55facf6e083ad6dc03c1dca Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:26:25 -0600 Subject: [PATCH 18/82] Use Addr interface everywhere and loop over arg sources --- src/node/cli.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index a5757aa6b..45aed3040 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -414,9 +414,9 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.auth = AuthType.Password } - const [host, port] = bindAddrFromAllSources(args, configArgs || { _: [] }) - args.host = host - args.port = port + const addr = bindAddrFromAllSources(args, configArgs || { _: [] }) + args.host = addr.host + args.port = addr.port // If we're being exposed to the cloud, we listen on a random address and // disable auth. @@ -512,12 +512,15 @@ export async function readConfigFile(configPath?: string): Promise { } } -function parseBindAddr(bindAddr: string): [string, number] { +function parseBindAddr(bindAddr: string): Addr { const u = new URL(`http://${bindAddr}`) - // With the http scheme 80 will be dropped so assume it's 80 if missing. This - // means --bind-addr without a port will default to 80 as well and not - // the code-server default. - return [u.hostname, u.port ? parseInt(u.port, 10) : 80] + return { + host: u.hostname, + // With the http scheme 80 will be dropped so assume it's 80 if missing. + // This means --bind-addr without a port will default to 80 as well + // and not the code-server default. + port: u.port ? parseInt(u.port, 10) : 80, + } } interface Addr { @@ -528,7 +531,7 @@ interface Addr { function bindAddrFromArgs(addr: Addr, args: Args): Addr { addr = { ...addr } if (args["bind-addr"]) { - ;[addr.host, addr.port] = parseBindAddr(args["bind-addr"]) + addr = parseBindAddr(args["bind-addr"]) } if (args.host) { addr.host = args.host @@ -543,16 +546,17 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr { return addr } -function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] { +function bindAddrFromAllSources(...argsConfig: Args[]): Addr { let addr: Addr = { host: "localhost", port: 8080, } - addr = bindAddrFromArgs(addr, configArgs) - addr = bindAddrFromArgs(addr, cliArgs) + for (const args of argsConfig) { + addr = bindAddrFromArgs(addr, args) + } - return [addr.host, addr.port] + return addr } async function copyOldMacOSDataDir(): Promise { From 3a074fd84460cab04d740bf4651da671fd0f4fac Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:30:34 -0600 Subject: [PATCH 19/82] Skip unnecessary auth type check when using --link --- src/node/cli.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 45aed3040..fcb74d805 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -425,10 +425,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.port = 0 args.socket = undefined args.cert = undefined - - if (args.auth !== AuthType.None) { - args.auth = AuthType.None - } + args.auth = AuthType.None } if (args.cert && !args.cert.value) { From f4e58553187e085fbee1fb3bfc3ed86ecf9633c6 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:31:32 -0600 Subject: [PATCH 20/82] Simplify update request --- src/node/update.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/node/update.ts b/src/node/update.ts index 42baa1848..2959b874c 100644 --- a/src/node/update.ts +++ b/src/node/update.ts @@ -105,24 +105,23 @@ export class UpdateProvider { logger.debug("Making request", field("uri", uri)) const httpx = uri.startsWith("https") ? https : http const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => { - if ( - response.statusCode && - response.statusCode >= 300 && - response.statusCode < 400 && - response.headers.location - ) { + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { + return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) + } + + if (response.statusCode >= 300) { ++redirects if (redirects > maxRedirects) { + response.destroy() return reject(new Error("reached max redirects")) } + if (!response.headers.location) { + return reject(new Error("received redirect with no location header")) + } response.destroy() return request(url.resolve(uri, response.headers.location)) } - if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) { - return reject(new Error(`${uri}: ${response.statusCode || "500"}`)) - } - resolve(response) }) client.on("error", reject) From c72c53f64de627801ae92e9911638031011a8d57 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:36:27 -0600 Subject: [PATCH 21/82] Fix not being able to dispose vscode after failed disposal --- src/node/vscode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 5cf4e4b76..9d935e663 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -22,8 +22,8 @@ export class VscodeProvider { if (this._vscode) { const vscode = await this._vscode vscode.removeAllListeners() - this._vscode = undefined vscode.kill() + this._vscode = undefined } } From c10450c4c5479af9e458c903fffe3f2ee19f53dd Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:40:06 -0600 Subject: [PATCH 22/82] Move isFile into util That allows its use in entry.ts as well. --- src/node/entry.ts | 18 ++++-------------- src/node/util.ts | 9 +++++++++ src/node/vscode.ts | 11 +---------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index df218a52d..82e04f5d9 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -1,6 +1,5 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" -import { promises as fs } from "fs" import http from "http" import * as path from "path" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" @@ -19,7 +18,7 @@ import { import { coderCloudBind } from "./coder-cloud" import { commit, version } from "./constants" import { register } from "./routes" -import { humanPath, open } from "./util" +import { humanPath, isFile, open } from "./util" import { ipcMain, WrapperProcess } from "./wrapper" export const runVsCodeCli = (args: DefaultedArgs): void => { @@ -55,21 +54,12 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st forceNewWindow: args["new-window"], } - const isDir = async (path: string): Promise => { - try { - const st = await fs.stat(path) - return st.isDirectory() - } catch (error) { - return false - } - } - for (let i = 0; i < args._.length; i++) { const fp = path.resolve(args._[i]) - if (await isDir(fp)) { - pipeArgs.folderURIs.push(fp) - } else { + if (await isFile(fp)) { pipeArgs.fileURIs.push(fp) + } else { + pipeArgs.folderURIs.push(fp) } } diff --git a/src/node/util.ts b/src/node/util.ts index 75122fe76..349c8edfb 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -261,3 +261,12 @@ export function canConnect(path: string): Promise { }) }) } + +export const isFile = async (path: string): Promise => { + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (error) { + return false + } +} diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 9d935e663..6e050de2f 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -1,12 +1,12 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" -import * as fs from "fs-extra" import * as net from "net" import * as path from "path" import * as ipc from "../../lib/vscode/src/vs/server/ipc" import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" +import { isFile } from "./util" export class VscodeProvider { public readonly serverRootPath: string @@ -123,15 +123,6 @@ export class VscodeProvider { private async getFirstPath( startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, ): Promise { - const isFile = async (path: string): Promise => { - try { - const stat = await fs.stat(path) - return stat.isFile() - } catch (error) { - logger.warn(error.message) - return false - } - } for (let i = 0; i < startPaths.length; ++i) { const startPath = startPaths[i] const url = arrayify(startPath && startPath.url).find((p) => !!p) From e243f6e36952cbe4d655cdbd77765b701feccd9f Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:42:37 -0600 Subject: [PATCH 23/82] Return early when forking to reduce indentation --- src/node/vscode.ts | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 6e050de2f..c25b7e9b0 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -74,30 +74,32 @@ export class VscodeProvider { } private fork(): Promise { - if (!this._vscode) { - logger.debug("forking vs code...") - const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) - this._vscode = undefined - }) - vscode.on("exit", (code) => { - logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined - }) - - this._vscode = new Promise((resolve, reject) => { - vscode.once("message", (message: ipc.VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) - }) + if (this._vscode) { + return this._vscode } + logger.debug("forking vs code...") + const vscode = cp.fork(path.join(this.serverRootPath, "fork")) + vscode.on("error", (error) => { + logger.error(error.message) + this._vscode = undefined + }) + vscode.on("exit", (code) => { + logger.error(`VS Code exited unexpectedly with code ${code}`) + this._vscode = undefined + }) + + this._vscode = new Promise((resolve, reject) => { + vscode.once("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vs code", field("message", message)) + return message.type === "ready" + ? resolve(vscode) + : reject(new Error("Unexpected response waiting for ready response")) + }) + vscode.once("error", reject) + vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + }) + return this._vscode } From 03e00131120263c38786410d48ff99f9a2144133 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 14:54:27 -0600 Subject: [PATCH 24/82] Unbind error/exit events once handshakes resolve --- src/node/vscode.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index c25b7e9b0..5cf826a16 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -51,14 +51,20 @@ export class VscodeProvider { logger.debug("setting up vs code...") return new Promise((resolve, reject) => { + const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) + vscode.once("message", (message: ipc.VscodeMessage) => { logger.debug("got message from vs code", field("message", message)) + vscode.off("error", reject) + vscode.off("exit", onExit) return message.type === "options" && message.id === id ? resolve(message.options) : reject(new Error("Unexpected response during initialization")) }) + vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + vscode.once("exit", onExit) + this.send( { type: "init", @@ -90,14 +96,19 @@ export class VscodeProvider { }) this._vscode = new Promise((resolve, reject) => { + const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) + vscode.once("message", (message: ipc.VscodeMessage) => { logger.debug("got message from vs code", field("message", message)) + vscode.off("error", reject) + vscode.off("exit", onExit) return message.type === "ready" ? resolve(vscode) : reject(new Error("Unexpected response waiting for ready response")) }) + vscode.once("error", reject) - vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`))) + vscode.once("exit", onExit) }) return this._vscode From 8066da12fe17f4d0f158ce5243e65ac0a0d4b189 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 15:37:16 -0600 Subject: [PATCH 25/82] Remove unused Locals interface --- src/node/http.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index 960669100..27164c0fb 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -9,13 +9,8 @@ import { HttpCode, HttpError } from "../common/http" import { normalize, Options } from "../common/util" import { AuthType } from "./cli" import { commit, rootPath } from "./constants" -import { Heart } from "./heart" import { hash } from "./util" -export interface Locals { - heart: Heart -} - /** * Replace common variable strings in HTML templates. */ From 75b93f9dc520ae8bc9256d3c6b02a6dcb8deebdc Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 4 Nov 2020 17:07:03 -0600 Subject: [PATCH 26/82] Fix bind address priority Broke when converting to a loop. --- src/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index fcb74d805..7a5b8d611 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -414,7 +414,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi args.auth = AuthType.Password } - const addr = bindAddrFromAllSources(args, configArgs || { _: [] }) + const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs) args.host = addr.host args.port = addr.port From e2c35facdbfd926ff624adf8ec3c07fe43692afd Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 15:47:20 -0600 Subject: [PATCH 27/82] Remove invalid comment on maybeProxy It no longer handles authentication. --- src/node/proxy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index a95badad9..d53de6927 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -25,8 +25,6 @@ export const router = Router() * * For example if `coder.com` is specified `8080.coder.com` will be proxied * but `8080.test.coder.com` and `test.8080.coder.com` will not. - * - * Throw an error if proxying but the user isn't authenticated. */ const maybeProxy = (req: Request): string | undefined => { // Split into parts. From a653b93ce210e1e160fbe906a777d77ca5b893bb Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:11:28 -0600 Subject: [PATCH 28/82] Include protocol on printed address This makes it clickable from the terminal. --- src/node/app.ts | 4 ++-- src/node/entry.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/node/app.ts b/src/node/app.ts index 5fde27675..171a7c4d0 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -45,7 +45,7 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Ser } /** - * Get the address of a server as a string (protocol not included) while + * Get the address of a server as a string (protocol *is* included) while * ensuring there is one (will throw if there isn't). */ export const ensureAddress = (server: http.Server): string => { @@ -54,7 +54,7 @@ export const ensureAddress = (server: http.Server): string => { throw new Error("server has no address") } if (typeof addr !== "string") { - return `${addr.address}:${addr.port}` + return `http://${addr.address}:${addr.port}` } return addr } diff --git a/src/node/entry.ts b/src/node/entry.ts index 82e04f5d9..3cca62a5b 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -141,7 +141,7 @@ const main = async (args: DefaultedArgs): Promise => { if (args.link) { try { - await coderCloudBind(serverAddress, args.link.value) + await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value) logger.info(" - Connected to cloud agent") } catch (err) { logger.error(err.message) From c5ce365482e1f10385605ef66b04838cdcf2818a Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:13:41 -0600 Subject: [PATCH 29/82] Use query variable to force update check --- src/node/routes/update.ts | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index ea7479fe0..b4fbc197e 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -12,19 +12,8 @@ router.use((req, _, next) => { next() }) -router.get("/", async (_, res) => { - const update = await provider.getUpdate() - res.json({ - checked: update.checked, - latest: update.version, - current: version, - isLatest: provider.isLatestVersion(update), - }) -}) - -// This route will force a check. -router.get("/check", async (_, res) => { - const update = await provider.getUpdate(true) +router.get("/", async (req, res) => { + const update = await provider.getUpdate(req.query.force === "true") res.json({ checked: update.checked, latest: update.version, From e5c8e0aad1c6fd11fb3039d7b7b77968caae0198 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:14:24 -0600 Subject: [PATCH 30/82] Remove useless || --- src/node/routes/vscode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index adef232fd..cec3a8321 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -14,7 +14,7 @@ const vscode = new VscodeProvider() router.get("/", async (req, res) => { if (!authenticated(req)) { return redirect(req, res, "login", { - to: req.baseUrl || "/", + to: req.baseUrl, }) } From 210fc049c40865a09f0ec747cd9d51b12208dd3c Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:30:45 -0600 Subject: [PATCH 31/82] Document VS Code endpoints --- src/node/routes/static.ts | 3 +++ src/node/routes/vscode.ts | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/node/routes/static.ts b/src/node/routes/static.ts index 0678c2313..e073219e4 100644 --- a/src/node/routes/static.ts +++ b/src/node/routes/static.ts @@ -32,6 +32,9 @@ router.get("/(:commit)(/*)?", async (req, res) => { res.header("Cache-Control", "public, max-age=31536000") } + /** + * Used by VS Code to load extensions into the web worker. + */ const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar if (typeof tar === "string") { let stream: Readable = tarFs.pack(pathToFsPath(tar)) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index cec3a8321..e7842a297 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -71,6 +71,9 @@ router.ws("/", async (socket, _, req) => { await vscode.sendWebsocket(socket, req.query) }) +/** + * TODO: Might currently be unused. + */ router.get("/resource(/*)?", async (req, res) => { ensureAuthenticated(req) if (typeof req.query.path === "string") { @@ -79,6 +82,9 @@ router.get("/resource(/*)?", async (req, res) => { } }) +/** + * Used by VS Code to load files. + */ router.get("/vscode-remote-resource(/*)?", async (req, res) => { ensureAuthenticated(req) if (typeof req.query.path === "string") { @@ -87,6 +93,10 @@ router.get("/vscode-remote-resource(/*)?", async (req, res) => { } }) +/** + * VS Code webviews use these paths to load files and to load webview assets + * like HTML and JavaScript. + */ router.get("/webview/*", async (req, res) => { ensureAuthenticated(req) res.set("Content-Type", getMediaMime(req.path)) From 476379a77e02f11f5261b1ec42cad67b1624a393 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:44:08 -0600 Subject: [PATCH 32/82] Fix cookie domain Had double Domain= --- src/node/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/http.ts b/src/node/http.ts index 27164c0fb..071ccd97e 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -130,7 +130,7 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | }) logger.debug("got cookie doman", field("host", host)) - return host ? `Domain=${host}` : undefined + return host || undefined } declare module "express" { From 34225e2bdf6f10a7a9b3b5fc25932ec8b8a8e17d Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 16:45:03 -0600 Subject: [PATCH 33/82] Use ensureAuthenticated as middleware --- src/node/http.ts | 43 ++++++++++++++++++++++++++------------- src/node/proxy.ts | 4 ++-- src/node/routes/proxy.ts | 4 ++-- src/node/routes/update.ts | 7 +------ src/node/routes/vscode.ts | 16 ++++++--------- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index 071ccd97e..42896f26b 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -34,12 +34,15 @@ export const replaceTemplates = ( } /** - * Throw an error if not authorized. + * Throw an error if not authorized. Call `next` if provided. */ -export const ensureAuthenticated = (req: express.Request): void => { +export const ensureAuthenticated = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => { if (!authenticated(req)) { throw new HttpError("Unauthorized", HttpCode.Unauthorized) } + if (next) { + next() + } } /** @@ -136,20 +139,32 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | declare module "express" { function Router(options?: express.RouterOptions): express.Router & WithWebsocketMethod - type WebsocketRequestHandler = ( - socket: net.Socket, - head: Buffer, - req: express.Request, + type WebSocketRequestHandler = ( + req: express.Request & WithWebSocket, + res: express.Response, next: express.NextFunction, ) => void | Promise - type WebsocketMethod = (route: expressCore.PathParams, ...handlers: WebsocketRequestHandler[]) => T + type WebSocketMethod = (route: expressCore.PathParams, ...handlers: WebSocketRequestHandler[]) => T + + interface WithWebSocket { + ws: net.Socket + head: Buffer + } interface WithWebsocketMethod { - ws: WebsocketMethod + ws: WebSocketMethod } } +interface WebsocketRequest extends express.Request, express.WithWebSocket { + _ws_handled: boolean +} + +function isWebSocketRequest(req: express.Request): req is WebsocketRequest { + return !!(req as WebsocketRequest).ws +} + export const handleUpgrade = (app: express.Express, server: http.Server): void => { server.on("upgrade", (req, socket, head) => { socket.on("error", () => socket.destroy()) @@ -193,15 +208,15 @@ function patchRouter(): void { // Inject the `ws` method. ;(express.Router as any).ws = function ws( route: expressCore.PathParams, - ...handlers: express.WebsocketRequestHandler[] + ...handlers: express.WebSocketRequestHandler[] ) { originalGet.apply(this, [ route, ...handlers.map((handler) => { - const wrapped: express.Handler = (req, _, next) => { - if ((req as any).ws) { - ;(req as any)._ws_handled = true - Promise.resolve(handler((req as any).ws, (req as any).head, req, next)).catch(next) + const wrapped: express.Handler = (req, res, next) => { + if (isWebSocketRequest(req)) { + req._ws_handled = true + Promise.resolve(handler(req, res, next)).catch(next) } else { next() } @@ -218,7 +233,7 @@ function patchRouter(): void { route, ...handlers.map((handler) => { const wrapped: express.Handler = (req, res, next) => { - if (!(req as any).ws) { + if (!isWebSocketRequest(req)) { Promise.resolve(handler(req, res, next)).catch(next) } else { next() diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d53de6927..bfc6af5b3 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -82,7 +82,7 @@ router.all("*", (req, res, next) => { }) }) -router.ws("*", (socket, head, req, next) => { +router.ws("*", (req, _, next) => { const port = maybeProxy(req) if (!port) { return next() @@ -91,7 +91,7 @@ router.ws("*", (socket, head, req, next) => { // Must be authenticated to use the proxy. ensureAuthenticated(req) - proxy.ws(req, socket, head, { + proxy.ws(req, req.ws, req.head, { ignorePath: true, target: `http://0.0.0.0:${port}${req.originalUrl}`, }) diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index 29aa999ae..59db92d97 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -35,8 +35,8 @@ router.all("/(:port)(/*)?", (req, res) => { }) }) -router.ws("/(:port)(/*)?", (socket, head, req) => { - proxy.ws(req, socket, head, { +router.ws("/(:port)(/*)?", (req) => { + proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true), }) diff --git a/src/node/routes/update.ts b/src/node/routes/update.ts index b4fbc197e..ac1ddc413 100644 --- a/src/node/routes/update.ts +++ b/src/node/routes/update.ts @@ -7,12 +7,7 @@ export const router = Router() const provider = new UpdateProvider() -router.use((req, _, next) => { - ensureAuthenticated(req) - next() -}) - -router.get("/", async (req, res) => { +router.get("/", ensureAuthenticated, async (req, res) => { const update = await provider.getUpdate(req.query.force === "true") res.json({ checked: update.checked, diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index e7842a297..c936571c5 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -53,14 +53,13 @@ router.get("/", async (req, res) => { ) }) -router.ws("/", async (socket, _, req) => { - ensureAuthenticated(req) +router.ws("/", ensureAuthenticated, async (req) => { const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" const reply = crypto .createHash("sha1") .update(req.headers["sec-websocket-key"] + magic) .digest("base64") - socket.write( + req.ws.write( [ "HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", @@ -68,14 +67,13 @@ router.ws("/", async (socket, _, req) => { `Sec-WebSocket-Accept: ${reply}`, ].join("\r\n") + "\r\n\r\n", ) - await vscode.sendWebsocket(socket, req.query) + await vscode.sendWebsocket(req.ws, req.query) }) /** * TODO: Might currently be unused. */ -router.get("/resource(/*)?", async (req, res) => { - ensureAuthenticated(req) +router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => { if (typeof req.query.path === "string") { res.set("Content-Type", getMediaMime(req.query.path)) res.send(await fs.readFile(pathToFsPath(req.query.path))) @@ -85,8 +83,7 @@ router.get("/resource(/*)?", async (req, res) => { /** * Used by VS Code to load files. */ -router.get("/vscode-remote-resource(/*)?", async (req, res) => { - ensureAuthenticated(req) +router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => { if (typeof req.query.path === "string") { res.set("Content-Type", getMediaMime(req.query.path)) res.send(await fs.readFile(pathToFsPath(req.query.path))) @@ -97,8 +94,7 @@ router.get("/vscode-remote-resource(/*)?", async (req, res) => { * VS Code webviews use these paths to load files and to load webview assets * like HTML and JavaScript. */ -router.get("/webview/*", async (req, res) => { - ensureAuthenticated(req) +router.get("/webview/*", ensureAuthenticated, async (req, res) => { res.set("Content-Type", getMediaMime(req.path)) if (/^vscode-resource/.test(req.params[0])) { return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, ""))) From 396af238421458ace5ca9fb0218fd9d02257fdae Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 3 Nov 2020 17:14:04 -0600 Subject: [PATCH 34/82] Kill VS Code when process exits This is to ensure it doesn't hang around. --- src/node/entry.ts | 4 ---- src/node/vscode.ts | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/entry.ts b/src/node/entry.ts index 3cca62a5b..c192158b6 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -102,10 +102,6 @@ const main = async (args: DefaultedArgs): Promise => { throw new Error("Please pass in a password via the config file or $PASSWORD") } - ipcMain.onDispose(() => { - // TODO: register disposables - }) - const [app, server] = await createApp(args) const serverAddress = ensureAddress(server) await register(app, server, args) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 5cf826a16..aed005f9f 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -7,6 +7,7 @@ import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" import { isFile } from "./util" +import { ipcMain } from "./wrapper" export class VscodeProvider { public readonly serverRootPath: string @@ -16,6 +17,7 @@ export class VscodeProvider { public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") + ipcMain.onDispose(() => this.dispose()) } public async dispose(): Promise { From 8252c372af84d8f5d2de40674af5eeda96e387e6 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 4 Nov 2020 16:49:01 -0600 Subject: [PATCH 35/82] Provide a way to tell when event handlers are finished This lets us actually wait for disposal before a graceful exit. --- src/common/emitter.ts | 25 ++++++++++++++++++++++--- src/common/types.ts | 1 - src/node/wrapper.ts | 7 ++++--- 3 files changed, 26 insertions(+), 7 deletions(-) delete mode 100644 src/common/types.ts diff --git a/src/common/emitter.ts b/src/common/emitter.ts index 7a1ebf668..353ce851e 100644 --- a/src/common/emitter.ts +++ b/src/common/emitter.ts @@ -1,4 +1,10 @@ -import { Callback } from "./types" +import { logger } from "@coder/logger" + +/** + * Event emitter callback. Called with the emitted value and a promise that + * resolves when all emitters have finished. + */ +export type Callback> = (t: T, p: Promise) => R export interface Disposable { dispose(): void @@ -32,8 +38,21 @@ export class Emitter { /** * Emit an event with a value. */ - public emit(value: T): void { - this.listeners.forEach((cb) => cb(value)) + public async emit(value: T): Promise { + let resolve: () => void + const promise = new Promise((r) => (resolve = r)) + + await Promise.all( + this.listeners.map(async (cb) => { + try { + await cb(value, promise) + } catch (error) { + logger.error(error.message) + } + }), + ) + + resolve!() } public dispose(): void { diff --git a/src/common/types.ts b/src/common/types.ts deleted file mode 100644 index a8a0e4c1b..000000000 --- a/src/common/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Callback = (t: T) => R diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index cce841901..2e8c51cd0 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -39,13 +39,14 @@ export class IpcMain { process.on("SIGTERM", () => this._onDispose.emit("SIGTERM")) process.on("exit", () => this._onDispose.emit(undefined)) - this.onDispose((signal) => { + this.onDispose((signal, wait) => { // Remove listeners to avoid possibly triggering disposal again. process.removeAllListeners() - // Let any other handlers run first then exit. + // Try waiting for other handlers run first then exit. logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal)) - setTimeout(() => this.exit(0), 0) + wait.then(() => this.exit(0)) + setTimeout(() => this.exit(0), 5000) }) // Kill the inner process if the parent dies. This is for the case where the From 9e09c1f92b27b4da5c38e56d282f62f646405358 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 11:36:27 -0600 Subject: [PATCH 36/82] Upgrade to Express 5 Now async routes are handled! --- package.json | 2 +- src/node/http.ts | 46 +++++++++++------------------------ src/node/routes/index.ts | 4 ---- yarn.lock | 52 ++++++++++++++++++++++++++++++---------- 4 files changed, 54 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 187052f8d..6459e6a0f 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "env-paths": "^2.2.0", - "express": "^4.17.1", + "express": "^5.0.0-alpha.8", "fs-extra": "^9.0.1", "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", diff --git a/src/node/http.ts b/src/node/http.ts index 42896f26b..71b938a25 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -191,22 +191,19 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void = } /** - * Patch Express routers to handle web sockets and async routes (since we have - * to patch `get` anyway). + * Patch Express routers to handle web sockets. * - * Not using express-ws since the ws-wrapped sockets don't work with the proxy - * and wildcards don't work correctly. + * Not using express-ws since the ws-wrapped sockets don't work with the proxy. */ function patchRouter(): void { - // Apparently this all works because Router is also the prototype assigned to - // the routers it returns. + // This works because Router is also the prototype assigned to the routers it + // returns. - // Store these since the original methods will be overridden. - const originalGet = (express.Router as any).get - const originalPost = (express.Router as any).post + // Store this since the original method will be overridden. + const originalGet = (express.Router as any).prototype.get // Inject the `ws` method. - ;(express.Router as any).ws = function ws( + ;(express.Router as any).prototype.ws = function ws( route: expressCore.PathParams, ...handlers: express.WebSocketRequestHandler[] ) { @@ -216,10 +213,9 @@ function patchRouter(): void { const wrapped: express.Handler = (req, res, next) => { if (isWebSocketRequest(req)) { req._ws_handled = true - Promise.resolve(handler(req, res, next)).catch(next) - } else { - next() + return handler(req, res, next) } + next() } return wrapped }), @@ -227,30 +223,16 @@ function patchRouter(): void { return this } // Overwrite `get` so we can distinguish between websocket and non-websocket - // routes. While we're at it handle async responses. - ;(express.Router as any).get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { + // routes. + ;(express.Router as any).prototype.get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { originalGet.apply(this, [ route, ...handlers.map((handler) => { const wrapped: express.Handler = (req, res, next) => { if (!isWebSocketRequest(req)) { - Promise.resolve(handler(req, res, next)).catch(next) - } else { - next() + return handler(req, res, next) } - } - return wrapped - }), - ]) - return this - } - // Handle async responses for `post` as well since we're in here anyway. - ;(express.Router as any).post = function post(route: expressCore.PathParams, ...handlers: express.Handler[]) { - originalPost.apply(this, [ - route, - ...handlers.map((handler) => { - const wrapped: express.Handler = (req, res, next) => { - Promise.resolve(handler(req, res, next)).catch(next) + next() } return wrapped }), @@ -259,5 +241,5 @@ function patchRouter(): void { } } -// This needs to happen before anything uses the router. +// This needs to happen before anything creates a router. patchRouter() diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 8c541d555..910f5b690 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -55,10 +55,6 @@ export const register = async (app: Express, server: http.Server, args: Defaulte app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) - server.on("upgrade", () => { - heart.beat() - }) - app.use(async (req, res, next) => { heart.beat() diff --git a/yarn.lock b/yarn.lock index 1d98caab6..d96b2e6ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1421,10 +1421,10 @@ array-equal@^1.0.0: resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= +array-flatten@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY= array-includes@^3.1.1: version "3.1.1" @@ -2585,6 +2585,13 @@ debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -3209,19 +3216,19 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== +express@^5.0.0-alpha.8: + version "5.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/express/-/express-5.0.0-alpha.8.tgz#b9dd3a568eab791e3391db47f9e6ab91e61b13fe" + integrity sha512-PL8wTLgaNOiq7GpXt187/yWHkrNSfbr4H0yy+V0fpqJt5wpUzBi9DprAkwGKBFOqWHylJ8EyPy34V5u9YArfng== dependencies: accepts "~1.3.7" - array-flatten "1.1.1" + array-flatten "2.1.1" body-parser "1.19.0" content-disposition "0.5.3" content-type "~1.0.4" cookie "0.4.0" cookie-signature "1.0.6" - debug "2.6.9" + debug "3.1.0" depd "~1.1.2" encodeurl "~1.0.2" escape-html "~1.0.3" @@ -3232,10 +3239,11 @@ express@^4.17.1: methods "~1.1.2" on-finished "~2.3.0" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-is-absolute "1.0.1" proxy-addr "~2.0.5" qs "6.7.0" range-parser "~1.2.1" + router "2.0.0-alpha.1" safe-buffer "5.1.2" send "0.17.1" serve-static "1.14.1" @@ -5489,7 +5497,7 @@ parse5@5.1.0: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== -parseurl@~1.3.3: +parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -5519,7 +5527,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -6619,6 +6627,19 @@ rotating-file-stream@^2.1.1: resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-2.1.3.tgz#4b3cc8f56ae70b3e30ccdb4ee6b14d95e66b02bb" integrity sha512-zZ4Tkngxispo7DgiTqX0s4ChLtM3qET6iYsDA9tmgDEqJ3BFgRq/ZotsKEDAYQt9pAn9JwwqT27CSwQt3CTxNg== +router@2.0.0-alpha.1: + version "2.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/router/-/router-2.0.0-alpha.1.tgz#9188213b972215e03ef830e0ac77837870085f6d" + integrity sha512-fz/T/qLkJM6RTtbqGqA1+uZ88ejqJoPyKeJAeXPYjebA7HzV/UyflH4gXWqW/Y6SERnp4kDwNARjqy6se3PcOw== + dependencies: + array-flatten "2.1.1" + debug "3.1.0" + methods "~1.1.2" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + setprototypeof "1.1.0" + utils-merge "1.0.1" + run-parallel@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" @@ -6736,6 +6757,11 @@ setimmediate@^1.0.4: resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" From 7b2752a62cd770d411aa9abb30b2082efc312dba Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 12:58:37 -0600 Subject: [PATCH 37/82] Move websocket routes into a separate app This is mostly so we don't have to do any wacky patching but it also makes it so we don't have to keep checking if the request is a web socket request every time we add middleware. --- src/node/app.ts | 9 ++-- src/node/entry.ts | 4 +- src/node/http.ts | 110 -------------------------------------- src/node/proxy.ts | 5 +- src/node/routes/index.ts | 52 ++++++++++++++---- src/node/routes/proxy.ts | 5 +- src/node/routes/vscode.ts | 37 +++++++------ src/node/wsRouter.ts | 57 ++++++++++++++++++++ 8 files changed, 134 insertions(+), 145 deletions(-) create mode 100644 src/node/wsRouter.ts diff --git a/src/node/app.ts b/src/node/app.ts index 171a7c4d0..448ec9660 100644 --- a/src/node/app.ts +++ b/src/node/app.ts @@ -4,12 +4,12 @@ import { promises as fs } from "fs" import http from "http" import * as httpolyglot from "httpolyglot" import { DefaultedArgs } from "./cli" -import { handleUpgrade } from "./http" +import { handleUpgrade } from "./wsRouter" /** * Create an Express app and an HTTP/S server to serve it. */ -export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Server]> => { +export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => { const app = express() const server = args.cert @@ -39,9 +39,10 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, http.Ser } }) - handleUpgrade(app, server) + const wsApp = express() + handleUpgrade(wsApp, server) - return [app, server] + return [app, wsApp, server] } /** diff --git a/src/node/entry.ts b/src/node/entry.ts index c192158b6..431aa8f49 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -102,9 +102,9 @@ const main = async (args: DefaultedArgs): Promise => { throw new Error("Please pass in a password via the config file or $PASSWORD") } - const [app, server] = await createApp(args) + const [app, wsApp, server] = await createApp(args) const serverAddress = ensureAddress(server) - await register(app, server, args) + 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)" : ""}`) diff --git a/src/node/http.ts b/src/node/http.ts index 71b938a25..f259d1037 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,8 +1,6 @@ import { field, logger } from "@coder/logger" import * as express from "express" import * as expressCore from "express-serve-static-core" -import * as http from "http" -import * as net from "net" import qs from "qs" import safeCompare from "safe-compare" import { HttpCode, HttpError } from "../common/http" @@ -135,111 +133,3 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string | logger.debug("got cookie doman", field("host", host)) return host || undefined } - -declare module "express" { - function Router(options?: express.RouterOptions): express.Router & WithWebsocketMethod - - type WebSocketRequestHandler = ( - req: express.Request & WithWebSocket, - res: express.Response, - next: express.NextFunction, - ) => void | Promise - - type WebSocketMethod = (route: expressCore.PathParams, ...handlers: WebSocketRequestHandler[]) => T - - interface WithWebSocket { - ws: net.Socket - head: Buffer - } - - interface WithWebsocketMethod { - ws: WebSocketMethod - } -} - -interface WebsocketRequest extends express.Request, express.WithWebSocket { - _ws_handled: boolean -} - -function isWebSocketRequest(req: express.Request): req is WebsocketRequest { - return !!(req as WebsocketRequest).ws -} - -export const handleUpgrade = (app: express.Express, server: http.Server): void => { - server.on("upgrade", (req, socket, head) => { - socket.on("error", () => socket.destroy()) - - req.ws = socket - req.head = head - req._ws_handled = false - - const res = new http.ServerResponse(req) - res.writeHead = function writeHead(statusCode: number) { - if (statusCode > 200) { - socket.destroy(new Error(`${statusCode}`)) - } - return res - } - - // Send the request off to be handled by Express. - ;(app as any).handle(req, res, () => { - if (!req._ws_handled) { - socket.destroy(new Error("Not found")) - } - }) - }) -} - -/** - * Patch Express routers to handle web sockets. - * - * Not using express-ws since the ws-wrapped sockets don't work with the proxy. - */ -function patchRouter(): void { - // This works because Router is also the prototype assigned to the routers it - // returns. - - // Store this since the original method will be overridden. - const originalGet = (express.Router as any).prototype.get - - // Inject the `ws` method. - ;(express.Router as any).prototype.ws = function ws( - route: expressCore.PathParams, - ...handlers: express.WebSocketRequestHandler[] - ) { - originalGet.apply(this, [ - route, - ...handlers.map((handler) => { - const wrapped: express.Handler = (req, res, next) => { - if (isWebSocketRequest(req)) { - req._ws_handled = true - return handler(req, res, next) - } - next() - } - return wrapped - }), - ]) - return this - } - // Overwrite `get` so we can distinguish between websocket and non-websocket - // routes. - ;(express.Router as any).prototype.get = function get(route: expressCore.PathParams, ...handlers: express.Handler[]) { - originalGet.apply(this, [ - route, - ...handlers.map((handler) => { - const wrapped: express.Handler = (req, res, next) => { - if (!isWebSocketRequest(req)) { - return handler(req, res, next) - } - next() - } - return wrapped - }), - ]) - return this - } -} - -// This needs to happen before anything creates a router. -patchRouter() diff --git a/src/node/proxy.ts b/src/node/proxy.ts index bfc6af5b3..4343d3346 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -2,6 +2,7 @@ import { Request, Router } from "express" import proxyServer from "http-proxy" import { HttpCode, HttpError } from "../common/http" import { authenticated, ensureAuthenticated } from "./http" +import { Router as WsRouter } from "./wsRouter" export const proxy = proxyServer.createProxyServer({}) proxy.on("error", (error, _, res) => { @@ -82,7 +83,9 @@ router.all("*", (req, res, next) => { }) }) -router.ws("*", (req, _, next) => { +export const wsRouter = WsRouter() + +wsRouter.ws("*", (req, _, next) => { const port = maybeProxy(req) if (!port) { return next() diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 910f5b690..8e5d3c181 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -1,7 +1,7 @@ import { logger } from "@coder/logger" import bodyParser from "body-parser" import cookieParser from "cookie-parser" -import { ErrorRequestHandler, Express } from "express" +import * as express from "express" import { promises as fs } from "fs" import http from "http" import * as path from "path" @@ -15,6 +15,7 @@ import { replaceTemplates } from "../http" import { loadPlugins } from "../plugin" import * as domainProxy from "../proxy" import { getMediaMime, paths } from "../util" +import { WebsocketRequest } from "../wsRouter" import * as health from "./health" import * as login from "./login" import * as proxy from "./proxy" @@ -36,7 +37,12 @@ declare global { /** * Register all routes and middleware. */ -export const register = async (app: Express, server: http.Server, args: DefaultedArgs): Promise => { +export const register = async ( + app: express.Express, + wsApp: express.Express, + server: http.Server, + args: DefaultedArgs, +): Promise => { const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { return new Promise((resolve, reject) => { server.getConnections((error, count) => { @@ -50,14 +56,28 @@ export const register = async (app: Express, server: http.Server, args: Defaulte }) app.disable("x-powered-by") + wsApp.disable("x-powered-by") app.use(cookieParser()) + wsApp.use(cookieParser()) + app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ extended: true })) - app.use(async (req, res, next) => { + const common: express.RequestHandler = (req, _, next) => { heart.beat() + // Add common variables routes can use. + req.args = args + req.heart = heart + + next() + } + + app.use(common) + wsApp.use(common) + + app.use(async (req, res, next) => { // If we're handling TLS ensure all requests are redirected to HTTPS. // TODO: This does *NOT* work if you have a base path since to specify the // protocol we need to specify the whole path. @@ -72,23 +92,28 @@ export const register = async (app: Express, server: http.Server, args: Defaulte return res.send(await fs.readFile(resourcePath)) } - // Add common variables routes can use. - req.args = args - req.heart = heart - - return next() + next() }) app.use("/", domainProxy.router) + wsApp.use("/", domainProxy.wsRouter.router) + app.use("/", vscode.router) + wsApp.use("/", vscode.wsRouter.router) + app.use("/vscode", vscode.router) + wsApp.use("/vscode", vscode.wsRouter.router) + app.use("/healthz", health.router) + if (args.auth === AuthType.Password) { app.use("/login", login.router) } + app.use("/proxy", proxy.router) + wsApp.use("/proxy", proxy.wsRouter.router) + app.use("/static", _static.router) app.use("/update", update.router) - app.use("/vscode", vscode.router) await loadPlugins(app, args) @@ -96,7 +121,7 @@ export const register = async (app: Express, server: http.Server, args: Defaulte throw new HttpError("Not Found", HttpCode.NotFound) }) - const errorHandler: ErrorRequestHandler = async (err, req, res, next) => { + const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") res.set("Content-Type", getMediaMime(resourcePath)) try { @@ -117,4 +142,11 @@ export const register = async (app: Express, server: http.Server, args: Defaulte } app.use(errorHandler) + + const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => { + logger.error(`${err.message} ${err.stack}`) + ;(req as WebsocketRequest).ws.destroy(err) + } + + wsApp.use(wsErrorHandler) } diff --git a/src/node/routes/proxy.ts b/src/node/routes/proxy.ts index 59db92d97..ff6f4067f 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/proxy.ts @@ -3,6 +3,7 @@ import qs from "qs" import { HttpCode, HttpError } from "../../common/http" import { authenticated, redirect } from "../http" import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" export const router = Router() @@ -35,7 +36,9 @@ router.all("/(:port)(/*)?", (req, res) => { }) }) -router.ws("/(:port)(/*)?", (req) => { +export const wsRouter = WsRouter() + +wsRouter.ws("/(:port)(/*)?", (req) => { proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true), diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index c936571c5..db2dc2071 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -6,6 +6,7 @@ import { commit, rootPath, version } from "../constants" import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http" import { getMediaMime, pathToFsPath } from "../util" import { VscodeProvider } from "../vscode" +import { Router as WsRouter } from "../wsRouter" export const router = Router() @@ -53,23 +54,6 @@ router.get("/", async (req, res) => { ) }) -router.ws("/", ensureAuthenticated, async (req) => { - const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - const reply = crypto - .createHash("sha1") - .update(req.headers["sec-websocket-key"] + magic) - .digest("base64") - req.ws.write( - [ - "HTTP/1.1 101 Switching Protocols", - "Upgrade: websocket", - "Connection: Upgrade", - `Sec-WebSocket-Accept: ${reply}`, - ].join("\r\n") + "\r\n\r\n", - ) - await vscode.sendWebsocket(req.ws, req.query) -}) - /** * TODO: Might currently be unused. */ @@ -103,3 +87,22 @@ router.get("/webview/*", ensureAuthenticated, async (req, res) => { await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])), ) }) + +export const wsRouter = WsRouter() + +wsRouter.ws("/", ensureAuthenticated, async (req) => { + const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + const reply = crypto + .createHash("sha1") + .update(req.headers["sec-websocket-key"] + magic) + .digest("base64") + req.ws.write( + [ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + `Sec-WebSocket-Accept: ${reply}`, + ].join("\r\n") + "\r\n\r\n", + ) + await vscode.sendWebsocket(req.ws, req.query) +}) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts new file mode 100644 index 000000000..1a057f0fa --- /dev/null +++ b/src/node/wsRouter.ts @@ -0,0 +1,57 @@ +import * as express from "express" +import * as expressCore from "express-serve-static-core" +import * as http from "http" +import * as net from "net" + +export const handleUpgrade = (app: express.Express, server: http.Server): void => { + server.on("upgrade", (req, socket, head) => { + socket.on("error", () => socket.destroy()) + + req.ws = socket + req.head = head + req._ws_handled = false + + // Send the request off to be handled by Express. + ;(app as any).handle(req, new http.ServerResponse(req), () => { + if (!req._ws_handled) { + socket.destroy(new Error("Not found")) + } + }) + }) +} + +export interface WebsocketRequest extends express.Request { + ws: net.Socket + head: Buffer +} + +interface InternalWebsocketRequest extends WebsocketRequest { + _ws_handled: boolean +} + +export type WebSocketHandler = ( + req: WebsocketRequest, + res: express.Response, + next: express.NextFunction, +) => void | Promise + +export class WebsocketRouter { + public readonly router = express.Router() + + public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void { + this.router.get( + route, + ...handlers.map((handler) => { + const wrapped: express.Handler = (req, res, next) => { + ;(req as InternalWebsocketRequest)._ws_handled = true + return handler(req as WebsocketRequest, res, next) + } + return wrapped + }), + ) + } +} + +export function Router(): WebsocketRouter { + return new WebsocketRouter() +} From 3f1750cf83ac23ef390e997cb039eb3ec30d2f58 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 14:34:57 -0600 Subject: [PATCH 38/82] Fix destroying response in update again I added another reject that doesn't destroy the response. --- src/node/update.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/node/update.ts b/src/node/update.ts index 2959b874c..13ac73c37 100644 --- a/src/node/update.ts +++ b/src/node/update.ts @@ -111,14 +111,13 @@ export class UpdateProvider { if (response.statusCode >= 300) { ++redirects + response.destroy() if (redirects > maxRedirects) { - response.destroy() return reject(new Error("reached max redirects")) } if (!response.headers.location) { return reject(new Error("received redirect with no location header")) } - response.destroy() return request(url.resolve(uri, response.headers.location)) } From cb991a9143137287321ae48861620565630103d3 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 15:19:15 -0600 Subject: [PATCH 39/82] Handle errors for JSON requests Previously it would have just given them the error HTML. --- src/node/routes/index.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 8e5d3c181..4643aa133 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -121,23 +121,29 @@ export const register = async ( throw new HttpError("Not Found", HttpCode.NotFound) }) - const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => { - const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") - res.set("Content-Type", getMediaMime(resourcePath)) - try { + const errorHandler: express.ErrorRequestHandler = async (err, req, res) => { + if (err.code === "ENOENT" || err.code === "EISDIR") { + err.status = HttpCode.NotFound + } + + const status = err.status ?? err.statusCode ?? 500 + res.status(status) + + if (req.accepts("application/json")) { + res.json({ + error: err.message, + ...(err.details || {}), + }) + } else { + const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html") + res.set("Content-Type", getMediaMime(resourcePath)) const content = await fs.readFile(resourcePath, "utf8") - if (err.code === "ENOENT" || err.code === "EISDIR") { - err.status = HttpCode.NotFound - } - const status = err.status ?? err.statusCode ?? 500 - res.status(status).send( + res.send( replaceTemplates(req, content) .replace(/{{ERROR_TITLE}}/g, status) .replace(/{{ERROR_HEADER}}/g, status) .replace(/{{ERROR_BODY}}/g, err.message), ) - } catch (error) { - next(error) } } From f6c4434191eb529aaa0f86e98365b490bb81a0d8 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 16:42:58 -0600 Subject: [PATCH 40/82] Tweak proxy fallthrough behavior It will now redirect all HTML requests. Also it avoids req.accepts since that's always truthy. --- src/node/proxy.ts | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index 4343d3346..d59df1d37 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import proxyServer from "http-proxy" import { HttpCode, HttpError } from "../common/http" -import { authenticated, ensureAuthenticated } from "./http" +import { authenticated, ensureAuthenticated, redirect } from "./http" import { Router as WsRouter } from "./wsRouter" export const proxy = proxyServer.createProxyServer({}) @@ -44,25 +44,6 @@ const maybeProxy = (req: Request): string | undefined => { return port } -/** - * Determine if the user is browsing /, /login, or static assets and if so fall - * through to allow the redirect and login flow. - */ -const shouldFallThrough = (req: Request): boolean => { - // See if it looks like a request for the root or login HTML. - if (req.accepts("text/html")) { - if ( - (req.path === "/" && req.method === "GET") || - (/\/login\/?/.test(req.path) && (req.method === "GET" || req.method === "POST")) - ) { - return true - } - } - - // See if it looks like a request for a static asset. - return req.path.startsWith("/static/") && req.method === "GET" -} - router.all("*", (req, res, next) => { const port = maybeProxy(req) if (!port) { @@ -71,9 +52,27 @@ router.all("*", (req, res, next) => { // Must be authenticated to use the proxy. if (!authenticated(req)) { - if (shouldFallThrough(req)) { + // Let the assets through since they're used on the login page. + if (req.path.startsWith("/static/") && req.method === "GET") { return next() } + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. + if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { + // Let the login through. + if (/\/login\/?/.test(req.path)) { + return next() + } + // Redirect all other pages to the login. + return redirect(req, res, "login", { + to: req.path, + }) + } + + // Everything else gets an unauthorized message. throw new HttpError("Unauthorized", HttpCode.Unauthorized) } From f7076247f96210ea0049897582a0c8b50bfdfea1 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 16:45:58 -0600 Subject: [PATCH 41/82] Move domain proxy to routes This matches better with the other routes. Also add a missing authentication check to the path proxy web socket. --- src/node/proxy.ts | 88 +--------------------- src/node/routes/domainProxy.ts | 87 +++++++++++++++++++++ src/node/routes/index.ts | 4 +- src/node/routes/{proxy.ts => pathProxy.ts} | 4 +- 4 files changed, 93 insertions(+), 90 deletions(-) create mode 100644 src/node/routes/domainProxy.ts rename src/node/routes/{proxy.ts => pathProxy.ts} (90%) diff --git a/src/node/proxy.ts b/src/node/proxy.ts index d59df1d37..da430f5b3 100644 --- a/src/node/proxy.ts +++ b/src/node/proxy.ts @@ -1,10 +1,8 @@ -import { Request, Router } from "express" import proxyServer from "http-proxy" -import { HttpCode, HttpError } from "../common/http" -import { authenticated, ensureAuthenticated, redirect } from "./http" -import { Router as WsRouter } from "./wsRouter" +import { HttpCode } from "../common/http" export const proxy = proxyServer.createProxyServer({}) + proxy.on("error", (error, _, res) => { res.writeHead(HttpCode.ServerError) res.end(error.message) @@ -16,85 +14,3 @@ proxy.on("proxyRes", (res, req) => { res.headers.location = (req as any).base + res.headers.location } }) - -export const router = Router() - -/** - * Return the port if the request should be proxied. Anything that ends in a - * proxy domain and has a *single* subdomain should be proxied. Anything else - * should return `undefined` and will be handled as normal. - * - * For example if `coder.com` is specified `8080.coder.com` will be proxied - * but `8080.test.coder.com` and `test.8080.coder.com` will not. - */ -const maybeProxy = (req: Request): string | undefined => { - // Split into parts. - const host = req.headers.host || "" - const idx = host.indexOf(":") - const domain = idx !== -1 ? host.substring(0, idx) : host - const parts = domain.split(".") - - // There must be an exact match. - const port = parts.shift() - const proxyDomain = parts.join(".") - if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { - return undefined - } - - return port -} - -router.all("*", (req, res, next) => { - const port = maybeProxy(req) - if (!port) { - return next() - } - - // Must be authenticated to use the proxy. - if (!authenticated(req)) { - // Let the assets through since they're used on the login page. - if (req.path.startsWith("/static/") && req.method === "GET") { - return next() - } - - // Assume anything that explicitly accepts text/html is a user browsing a - // page (as opposed to an xhr request). Don't use `req.accepts()` since - // *every* request that I've seen (in Firefox and Chromium at least) - // includes `*/*` making it always truthy. - if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { - // Let the login through. - if (/\/login\/?/.test(req.path)) { - return next() - } - // Redirect all other pages to the login. - return redirect(req, res, "login", { - to: req.path, - }) - } - - // Everything else gets an unauthorized message. - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - - proxy.web(req, res, { - ignorePath: true, - target: `http://0.0.0.0:${port}${req.originalUrl}`, - }) -}) - -export const wsRouter = WsRouter() - -wsRouter.ws("*", (req, _, next) => { - const port = maybeProxy(req) - if (!port) { - return next() - } - - // Must be authenticated to use the proxy. - ensureAuthenticated(req) - - proxy.ws(req, req.ws, req.head, { - ignorePath: true, - target: `http://0.0.0.0:${port}${req.originalUrl}`, - }) -}) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts new file mode 100644 index 000000000..ac249b809 --- /dev/null +++ b/src/node/routes/domainProxy.ts @@ -0,0 +1,87 @@ +import { Request, Router } from "express" +import { HttpCode, HttpError } from "../../common/http" +import { authenticated, ensureAuthenticated, redirect } from "../http" +import { proxy } from "../proxy" +import { Router as WsRouter } from "../wsRouter" + +export const router = Router() + +/** + * Return the port if the request should be proxied. Anything that ends in a + * proxy domain and has a *single* subdomain should be proxied. Anything else + * should return `undefined` and will be handled as normal. + * + * For example if `coder.com` is specified `8080.coder.com` will be proxied + * but `8080.test.coder.com` and `test.8080.coder.com` will not. + */ +const maybeProxy = (req: Request): string | undefined => { + // Split into parts. + const host = req.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host + const parts = domain.split(".") + + // There must be an exact match. + const port = parts.shift() + const proxyDomain = parts.join(".") + if (!port || !req.args["proxy-domain"].includes(proxyDomain)) { + return undefined + } + + return port +} + +router.all("*", (req, res, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + if (!authenticated(req)) { + // Let the assets through since they're used on the login page. + if (req.path.startsWith("/static/") && req.method === "GET") { + return next() + } + + // Assume anything that explicitly accepts text/html is a user browsing a + // page (as opposed to an xhr request). Don't use `req.accepts()` since + // *every* request that I've seen (in Firefox and Chromium at least) + // includes `*/*` making it always truthy. + if (typeof req.headers.accepts === "string" && req.headers.accepts.split(",").includes("text/html")) { + // Let the login through. + if (/\/login\/?/.test(req.path)) { + return next() + } + // Redirect all other pages to the login. + return redirect(req, res, "login", { + to: req.path, + }) + } + + // Everything else gets an unauthorized message. + throw new HttpError("Unauthorized", HttpCode.Unauthorized) + } + + proxy.web(req, res, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) + +export const wsRouter = WsRouter() + +wsRouter.ws("*", (req, _, next) => { + const port = maybeProxy(req) + if (!port) { + return next() + } + + // Must be authenticated to use the proxy. + ensureAuthenticated(req) + + proxy.ws(req, req.ws, req.head, { + ignorePath: true, + target: `http://0.0.0.0:${port}${req.originalUrl}`, + }) +}) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 4643aa133..afb24f156 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -13,12 +13,12 @@ import { rootPath } from "../constants" import { Heart } from "../heart" import { replaceTemplates } from "../http" import { loadPlugins } from "../plugin" -import * as domainProxy from "../proxy" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" +import * as domainProxy from "./domainProxy" import * as health from "./health" import * as login from "./login" -import * as proxy from "./proxy" +import * as proxy from "./pathProxy" // static is a reserved keyword. import * as _static from "./static" import * as update from "./update" diff --git a/src/node/routes/proxy.ts b/src/node/routes/pathProxy.ts similarity index 90% rename from src/node/routes/proxy.ts rename to src/node/routes/pathProxy.ts index ff6f4067f..d21d08eb8 100644 --- a/src/node/routes/proxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,7 +1,7 @@ import { Request, Router } from "express" import qs from "qs" import { HttpCode, HttpError } from "../../common/http" -import { authenticated, redirect } from "../http" +import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -38,7 +38,7 @@ router.all("/(:port)(/*)?", (req, res) => { export const wsRouter = WsRouter() -wsRouter.ws("/(:port)(/*)?", (req) => { +wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => { proxy.ws(req, req.ws, req.head, { ignorePath: true, target: getProxyTarget(req, true), From 959497067c32e265661f1d70c864e793b0e96084 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 5 Nov 2020 17:07:51 -0600 Subject: [PATCH 42/82] Document HttpError Also type the status. --- src/common/http.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/http.ts b/src/common/http.ts index 5279bf44f..c08c8673b 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -8,8 +8,12 @@ export enum HttpCode { ServerError = 500, } +/** + * Represents an error with a message and an HTTP status code. This code will be + * used in the HTTP response. + */ export class HttpError extends Error { - public constructor(message: string, public readonly status: number, public readonly details?: object) { + public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) { super(message) this.name = this.constructor.name } From aa2cfa2c17ebc91e87f2a0df442cecff7fe55e6a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Oct 2020 23:17:28 -0400 Subject: [PATCH 43/82] typings/plugin.d.ts: Create --- typings/plugin.d.ts | 110 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 typings/plugin.d.ts diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts new file mode 100644 index 000000000..92c3acada --- /dev/null +++ b/typings/plugin.d.ts @@ -0,0 +1,110 @@ +/** + * This file describes the code-server plugin API for adding new applications. + */ +import { Logger } from "@coder/logger" +import * as express from "express" + +/** + * Overlay + * + * The homepage of code-server will launch into VS Code. However, there will be an overlay + * button that when clicked, will show all available applications with their names, + * icons and provider plugins. When one clicks on an app's icon, they will be directed + * to // to access the application. + */ + +/** + * Plugins + * + * Plugins are just node modules. + * + * code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * $CS_PLUGIN_PATH with a package.json where the engine is code-server is + * a valid plugin. + * + * e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search + * /tmp/nhooyr and then /tmp/ash for plugins. + * + * CS_PLUGIN_PATH defaults to + * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins + * if unset. + * + * code-server also uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * Built in plugins are also loaded from __dirname/../plugins + * + * Priority is $CS_PLUGIN, $CS_PLUGIN_PATH and then the builtin plugins. + * After the search is complete, plugins will be required in first found order and + * initialized. See the Plugin interface for details. + * + * There is also a /api/applications endpoint to allow programmatic access to all + * available applications. It could be used to create a custom application dashboard + * for example. + */ + +/** + * Your plugin module must implement this interface. + * + * The plugin's name, description and version are fetched from its module's package.json + * + * The plugin's router will be mounted at / + * + * If two plugins are found with the exact same name, then code-server will + * use the last one and emit a warning. + */ +export interface Plugin { + /** + * init is called so that the plugin may initialize itself with the config. + */ + init(config: PluginConfig): void + + /** + * Returns the plugin's router. + */ + router(): express.Router + + /** + * code-server uses this to collect the list of applications that + * the plugin can currently provide. + * It is called when /api/applications is hit or the overlay needs to + * refresh the list of applications + * + * Ensure this is as fast as possible. + */ + applications(): Application[] | Promise +} + +/** + * PluginConfig contains the configuration required for initializing + * a plugin. + */ +export interface PluginConfig { + /** + * All plugin logs should be logged via this logger. + */ + readonly logger: Logger +} + +/** + * Application represents a user accessible application. + * + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If the app's name is the same as the plugin's name then + * / will be used instead. + */ +export interface Application { + readonly name: string + readonly version: string + + /** + * The path at which the icon for this application can be accessed. + * /// + */ + readonly iconPath: string +} From 481df70622e260b8628af76c70933db419ac4f33 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Oct 2020 23:18:07 -0400 Subject: [PATCH 44/82] ci/dev/test.sh: Pass through args --- ci/dev/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 031bacf99..983b2f292 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,7 +4,7 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - mocha -r ts-node/register ./test/*.test.ts + mocha -r ts-node/register ./test/*.test.ts "$@" } main "$@" From e08a55d44a1067c54cc845efd54cc31271c3ae08 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:18:45 -0400 Subject: [PATCH 45/82] src/node/plugin.ts: Implement new plugin API --- src/node/plugin.ts | 239 ++++++++++++++++++++++++++------------- src/node/routes/index.ts | 6 +- 2 files changed, 166 insertions(+), 79 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 20c19d3e7..2ae29b967 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,92 +1,177 @@ -import { field, logger } from "@coder/logger" -import { Express } from "express" -import * as fs from "fs" import * as path from "path" -import * as util from "util" -import { Args } from "./cli" -import { paths } from "./util" +import * as util from "./util" +import * as pluginapi from "../../typings/plugin" +import * as fs from "fs" +import * as semver from "semver" +import { version } from "./constants" +const fsp = fs.promises +import { Logger, field } from "@coder/logger" +import * as express from "express" -/* eslint-disable @typescript-eslint/no-var-requires */ +// These fields are populated from the plugin's package.json. +interface Plugin extends pluginapi.Plugin { + name: string + version: string + description: string +} -export type Activate = (app: Express, args: Args) => void - -/** - * Plugins must implement this interface. - */ -export interface Plugin { - activate: Activate +interface Application extends pluginapi.Application { + plugin: Plugin } /** - * Intercept imports so we can inject code-server when the plugin tries to - * import it. + * PluginAPI implements the plugin API described in typings/plugin.d.ts + * Please see that file for details. */ -const originalLoad = require("module")._load -// eslint-disable-next-line @typescript-eslint/no-explicit-any -require("module")._load = function (request: string, parent: object, isMain: boolean): any { - return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) -} +export class PluginAPI { + private readonly plugins = new Array() + private readonly logger: Logger -/** - * Load a plugin and run its activation function. - */ -const loadPlugin = async (pluginPath: string, app: Express, args: Args): Promise => { - try { - const plugin: Plugin = require(pluginPath) - plugin.activate(app, args) - - const packageJson = require(path.join(pluginPath, "package.json")) - logger.debug( - "Loaded plugin", - field("name", packageJson.name || path.basename(pluginPath)), - field("path", pluginPath), - field("version", packageJson.version || "n/a"), - ) - } catch (error) { - logger.error(error.message) + public constructor( + logger: Logger, + /** + * These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively. + */ + private readonly csPlugin = "", + private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, + ){ + this.logger = logger.named("pluginapi") } -} -/** - * Load all plugins in the specified directory. - */ -const _loadPlugins = async (pluginDir: string, app: Express, args: Args): Promise => { - try { - const files = await util.promisify(fs.readdir)(pluginDir, { - withFileTypes: true, - }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), app, args))) - } catch (error) { - if (error.code !== "ENOENT") { - logger.warn(error.message) + /** + * applications grabs the full list of applications from + * all loaded plugins. + */ + public async applications(): Promise { + const apps = new Array() + for (let p of this.plugins) { + const pluginApps = await p.applications() + + // TODO prevent duplicates + // Add plugin key to each app. + apps.push( + ...pluginApps.map((app) => { + return { ...app, plugin: p } + }), + ) + } + return apps + } + + /** + * mount mounts all plugin routers onto r. + */ + public mount(r: express.Router): void { + for (let p of this.plugins) { + r.use(`/${p.name}`, p.router()) } } + + /** + * loadPlugins loads all plugins based on this.csPluginPath + * and this.csPlugin. + */ + public async loadPlugins(): Promise { + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) + + for (let dir of this.csPluginPath.split(":")) { + if (!dir) { + continue + } + await this._loadPlugins(dir) + } + + for (let dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } + } + + private async _loadPlugins(dir: string): Promise { + try { + const entries = await fsp.readdir(dir, { withFileTypes: true }) + for (let ent of entries) { + if (!ent.isDirectory()) { + continue + } + await this.loadPlugin(path.join(dir, ent.name)) + } + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`) + } + } + } + + private async loadPlugin(dir: string): Promise { + try { + const str = await fsp.readFile(path.join(dir, "package.json"), { + encoding: "utf8", + }) + const packageJSON: PackageJSON = JSON.parse(str) + const p = this._loadPlugin(dir, packageJSON) + // TODO prevent duplicates + this.plugins.push(p) + } catch (err) { + if (err.code !== "ENOENT") { + this.logger.warn(`failed to load plugin: ${err.message}`) + } + } + } + + private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + const logger = this.logger.named(packageJSON.name) + logger.debug("loading plugin", + field("plugin_dir", dir), + field("package_json", packageJSON), + ) + + if (!semver.satisfies(version, packageJSON.engines["code-server"])) { + throw new Error(`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + + ` with code-server version ${version}`) + } + if (!packageJSON.name) { + throw new Error("plugin missing name") + } + if (!packageJSON.version) { + throw new Error("plugin missing version") + } + if (!packageJSON.description) { + throw new Error("plugin missing description") + } + + const p = { + name: packageJSON.name, + version: packageJSON.version, + description: packageJSON.description, + ...require(dir), + } as Plugin + + p.init({ + logger: logger, + }) + + logger.debug("loaded") + + return p + } } -/** - * Load all plugins from the `plugins` directory, directories specified by - * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by - * `CS_PLUGIN` (also colon-separated). - */ -export const loadPlugins = async (app: Express, args: Args): Promise => { - const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins` - const plugin = process.env.CS_PLUGIN || "" - await Promise.all([ - // Built-in plugins. - _loadPlugins(path.resolve(__dirname, "../../plugins"), app, args), - // User-added plugins. - ...pluginPath - .split(":") - .filter((p) => !!p) - .map((dir) => _loadPlugins(path.resolve(dir), app, args)), - // Individual plugins so you don't have to symlink or move them into a - // directory specifically for plugins. This lets you load plugins that are - // on the same level as other directories that are not plugins (if you tried - // to use CS_PLUGIN_PATH code-server would try to load those other - // directories as plugins). Intended for development. - ...plugin - .split(":") - .filter((p) => !!p) - .map((dir) => loadPlugin(path.resolve(dir), app, args)), - ]) +interface PackageJSON { + name: string + version: string + description: string + engines: { + "code-server": string + } +} + +function q(s: string): string { + if (s === undefined) { + s = "undefined" + } + return JSON.stringify(s) } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index afb24f156..5824475d9 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -12,7 +12,7 @@ import { AuthType, DefaultedArgs } from "../cli" import { rootPath } from "../constants" import { Heart } from "../heart" import { replaceTemplates } from "../http" -import { loadPlugins } from "../plugin" +import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" @@ -115,7 +115,9 @@ export const register = async ( app.use("/static", _static.router) app.use("/update", update.router) - await loadPlugins(app, args) + const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) + await papi.loadPlugins() + papi.mount(app) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From bea185b8b2cf3487e4dc63c95d822bca76d3a030 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:18:59 -0400 Subject: [PATCH 46/82] plugin: Add basic loading test Will work on testing overlay next. --- test/plugin.test.ts | 31 +++ test/test-plugin/.gitignore | 1 + test/test-plugin/package.json | 20 ++ test/test-plugin/public/icon.svg | 1 + test/test-plugin/src/index.ts | 23 ++ test/test-plugin/tsconfig.json | 69 +++++ test/test-plugin/yarn.lock | 435 +++++++++++++++++++++++++++++++ 7 files changed, 580 insertions(+) create mode 100644 test/plugin.test.ts create mode 100644 test/test-plugin/.gitignore create mode 100644 test/test-plugin/package.json create mode 100644 test/test-plugin/public/icon.svg create mode 100644 test/test-plugin/src/index.ts create mode 100644 test/test-plugin/tsconfig.json create mode 100644 test/test-plugin/yarn.lock diff --git a/test/plugin.test.ts b/test/plugin.test.ts new file mode 100644 index 000000000..05a72028a --- /dev/null +++ b/test/plugin.test.ts @@ -0,0 +1,31 @@ +import { describe } from "mocha" +import { PluginAPI } from "../src/node/plugin" +import { logger } from "@coder/logger" +import * as path from "path" +import * as assert from "assert" + +/** + * Use $LOG_LEVEL=debug to see debug logs. + */ +describe("plugin", () => { + it("loads", async () => { + const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + await papi.loadPlugins() + + // We remove the function fields from the application's plugins. + const apps = JSON.parse(JSON.stringify(await papi.applications())) + + assert.deepEqual([ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + plugin: { + name: "test-plugin", + version: "1.0.0", + description: "Fake plugin for testing code-server's plugin API", + }, + }, + ], apps) + }) +}) diff --git a/test/test-plugin/.gitignore b/test/test-plugin/.gitignore new file mode 100644 index 000000000..1fcb1529f --- /dev/null +++ b/test/test-plugin/.gitignore @@ -0,0 +1 @@ +out diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json new file mode 100644 index 000000000..ccdeabb56 --- /dev/null +++ b/test/test-plugin/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "name": "test-plugin", + "version": "1.0.0", + "description": "Fake plugin for testing code-server's plugin API", + "engines": { + "code-server": "^3.6.0" + }, + "main": "out/index.js", + "devDependencies": { + "@types/express": "^4.17.8", + "typescript": "^4.0.5" + }, + "scripts": { + "build": "tsc" + }, + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/test/test-plugin/public/icon.svg b/test/test-plugin/public/icon.svg new file mode 100644 index 000000000..25b9cf047 --- /dev/null +++ b/test/test-plugin/public/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts new file mode 100644 index 000000000..b601cb3c0 --- /dev/null +++ b/test/test-plugin/src/index.ts @@ -0,0 +1,23 @@ +import * as pluginapi from "../../../typings/plugin" +import * as express from "express" +import * as path from "path"; + +export function init(config: pluginapi.PluginConfig) { + config.logger.debug("test-plugin loaded!") +} + +export function router(): express.Router { + const r = express.Router() + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(path.resolve(__dirname, "../public/icon.svg")) + }) + return r +} + +export function applications(): pluginapi.Application[] { + return [{ + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + }] +} diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json new file mode 100644 index 000000000..86e4897bb --- /dev/null +++ b/test/test-plugin/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./out", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/test/test-plugin/yarn.lock b/test/test-plugin/yarn.lock new file mode 100644 index 000000000..c77db2f7e --- /dev/null +++ b/test/test-plugin/yarn.lock @@ -0,0 +1,435 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" + integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/node@*": + version "14.14.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f" + integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw== + +"@types/qs@*": + version "6.9.5" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" + integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.6" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1" + integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= From 82e8a00a0d9dff6bcb4ca754ded9dbe1f42407cd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:26:30 -0400 Subject: [PATCH 47/82] Fix CI --- src/node/plugin.ts | 36 ++++++++++++++++------------------ test/plugin.test.ts | 31 ++++++++++++++++------------- test/test-plugin/src/index.ts | 16 ++++++++------- test/test-plugin/tsconfig.json | 14 ++++++------- 4 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 2ae29b967..ab2af5d66 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -1,12 +1,12 @@ -import * as path from "path" -import * as util from "./util" -import * as pluginapi from "../../typings/plugin" -import * as fs from "fs" -import * as semver from "semver" -import { version } from "./constants" -const fsp = fs.promises import { Logger, field } from "@coder/logger" import * as express from "express" +import * as fs from "fs" +import * as path from "path" +import * as semver from "semver" +import * as pluginapi from "../../typings/plugin" +import { version } from "./constants" +import * as util from "./util" +const fsp = fs.promises // These fields are populated from the plugin's package.json. interface Plugin extends pluginapi.Plugin { @@ -34,7 +34,7 @@ export class PluginAPI { */ private readonly csPlugin = "", private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`, - ){ + ) { this.logger = logger.named("pluginapi") } @@ -44,7 +44,7 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (let p of this.plugins) { + for (const p of this.plugins) { const pluginApps = await p.applications() // TODO prevent duplicates @@ -62,7 +62,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (let p of this.plugins) { + for (const p of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -75,14 +75,14 @@ export class PluginAPI { // Built-in plugins. await this._loadPlugins(path.join(__dirname, "../../plugins")) - for (let dir of this.csPluginPath.split(":")) { + for (const dir of this.csPluginPath.split(":")) { if (!dir) { continue } await this._loadPlugins(dir) } - for (let dir of this.csPlugin.split(":")) { + for (const dir of this.csPlugin.split(":")) { if (!dir) { continue } @@ -93,7 +93,7 @@ export class PluginAPI { private async _loadPlugins(dir: string): Promise { try { const entries = await fsp.readdir(dir, { withFileTypes: true }) - for (let ent of entries) { + for (const ent of entries) { if (!ent.isDirectory()) { continue } @@ -124,14 +124,12 @@ export class PluginAPI { private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { const logger = this.logger.named(packageJSON.name) - logger.debug("loading plugin", - field("plugin_dir", dir), - field("package_json", packageJSON), - ) + logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) if (!semver.satisfies(version, packageJSON.engines["code-server"])) { - throw new Error(`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + - ` with code-server version ${version}`) + throw new Error( + `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, + ) } if (!packageJSON.name) { throw new Error("plugin missing name") diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 05a72028a..a77b1cf43 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,8 +1,8 @@ -import { describe } from "mocha" -import { PluginAPI } from "../src/node/plugin" import { logger } from "@coder/logger" -import * as path from "path" import * as assert from "assert" +import { describe } from "mocha" +import * as path from "path" +import { PluginAPI } from "../src/node/plugin" /** * Use $LOG_LEVEL=debug to see debug logs. @@ -15,17 +15,20 @@ describe("plugin", () => { // We remove the function fields from the application's plugins. const apps = JSON.parse(JSON.stringify(await papi.applications())) - assert.deepEqual([ - { - name: "goland", - version: "4.0.0", - iconPath: "icon.svg", - plugin: { - name: "test-plugin", - version: "1.0.0", - description: "Fake plugin for testing code-server's plugin API", + assert.deepEqual( + [ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + plugin: { + name: "test-plugin", + version: "1.0.0", + description: "Fake plugin for testing code-server's plugin API", + }, }, - }, - ], apps) + ], + apps, + ) }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index b601cb3c0..83575533b 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,6 +1,6 @@ -import * as pluginapi from "../../../typings/plugin" import * as express from "express" -import * as path from "path"; +import * as path from "path" +import * as pluginapi from "../../../typings/plugin" export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") @@ -15,9 +15,11 @@ export function router(): express.Router { } export function applications(): pluginapi.Application[] { - return [{ - name: "goland", - version: "4.0.0", - iconPath: "icon.svg", - }] + return [ + { + name: "goland", + version: "4.0.0", + iconPath: "icon.svg", + }, + ] } diff --git a/test/test-plugin/tsconfig.json b/test/test-plugin/tsconfig.json index 86e4897bb..0956ead88 100644 --- a/test/test-plugin/tsconfig.json +++ b/test/test-plugin/tsconfig.json @@ -4,8 +4,8 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ @@ -14,7 +14,7 @@ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./out", /* Redirect output structure to the directory. */ + "outDir": "./out" /* Redirect output structure to the directory. */, // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ @@ -25,7 +25,7 @@ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ @@ -48,7 +48,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ @@ -63,7 +63,7 @@ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ - "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "skipLibCheck": true /* Skip type checking of declaration files. */, + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } } From 30d2962e21468825e545235c3e272c6a7ffefe97 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:37:42 -0400 Subject: [PATCH 48/82] src/node/plugin.ts: Warn on duplicate plugin and only load first --- src/node/plugin.ts | 20 +++++++++++++++++--- test/plugin.test.ts | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ab2af5d66..ddfef7f97 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -8,11 +8,18 @@ import { version } from "./constants" import * as util from "./util" const fsp = fs.promises -// These fields are populated from the plugin's package.json. interface Plugin extends pluginapi.Plugin { + /** + * These fields are populated from the plugin's package.json. + */ name: string version: string description: string + + /** + * path to the node module on the disk. + */ + modulePath: string } interface Application extends pluginapi.Application { @@ -47,7 +54,6 @@ export class PluginAPI { for (const p of this.plugins) { const pluginApps = await p.applications() - // TODO prevent duplicates // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { @@ -112,8 +118,15 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) + for (const p of this.plugins) { + if (p.name === packageJSON.name) { + this.logger.warn( + `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, + ) + return + } + } const p = this._loadPlugin(dir, packageJSON) - // TODO prevent duplicates this.plugins.push(p) } catch (err) { if (err.code !== "ENOENT") { @@ -145,6 +158,7 @@ export class PluginAPI { name: packageJSON.name, version: packageJSON.version, description: packageJSON.description, + modulePath: dir, ...require(dir), } as Plugin diff --git a/test/plugin.test.ts b/test/plugin.test.ts index a77b1cf43..014e07f5d 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -25,6 +25,7 @@ describe("plugin", () => { name: "test-plugin", version: "1.0.0", description: "Fake plugin for testing code-server's plugin API", + modulePath: path.join(__dirname, "test-plugin"), }, }, ], From ef971009d9632e333e2428fc3214dd2fa6f9ac02 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Oct 2020 03:39:14 -0400 Subject: [PATCH 49/82] plugin.test.ts: Make it clear iconPath is a path --- test/plugin.test.ts | 2 +- test/test-plugin/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 014e07f5d..69c4572ee 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -20,7 +20,7 @@ describe("plugin", () => { { name: "goland", version: "4.0.0", - iconPath: "icon.svg", + iconPath: "/icon.svg", plugin: { name: "test-plugin", version: "1.0.0", diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 83575533b..94bf73b80 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -19,7 +19,7 @@ export function applications(): pluginapi.Application[] { { name: "goland", version: "4.0.0", - iconPath: "icon.svg", + iconPath: "/icon.svg", }, ] } From f4d7f000331fe8152bfb95b7552c031e41fb3cd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:21:18 -0500 Subject: [PATCH 50/82] plugin.ts: Fixes for @wbobeirne --- src/node/plugin.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ddfef7f97..cdd9c3d9a 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -150,9 +150,6 @@ export class PluginAPI { if (!packageJSON.version) { throw new Error("plugin missing version") } - if (!packageJSON.description) { - throw new Error("plugin missing description") - } const p = { name: packageJSON.name, @@ -181,7 +178,7 @@ interface PackageJSON { } } -function q(s: string): string { +function q(s: string | undefined): string { if (s === undefined) { s = "undefined" } From 75e52a37742833679711fca2b48fdf8a04fcb521 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:24:06 -0500 Subject: [PATCH 51/82] plugin.ts: Fixes for @code-asher --- ci/dev/test.sh | 3 +++ src/node/plugin.ts | 19 +++++++++++++++---- test/plugin.test.ts | 3 +-- test/test-plugin/Makefile | 5 +++++ 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 test/test-plugin/Makefile diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 983b2f292..6eaa3878d 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -4,6 +4,9 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." + cd test/test-plugin + make -s out/index.js + cd $OLDPWD mocha -r ts-node/register ./test/*.test.ts "$@" } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index cdd9c3d9a..f0dca2754 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -23,7 +23,10 @@ interface Plugin extends pluginapi.Plugin { } interface Application extends pluginapi.Application { - plugin: Plugin + /* + * Clone of the above without functions. + */ + plugin: Omit } /** @@ -57,7 +60,15 @@ export class PluginAPI { // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { - return { ...app, plugin: p } + return { + ...app, + plugin: { + name: p.name, + version: p.version, + description: p.description, + modulePath: p.modulePath, + }, + } }), ) } @@ -74,8 +85,8 @@ export class PluginAPI { } /** - * loadPlugins loads all plugins based on this.csPluginPath - * and this.csPlugin. + * loadPlugins loads all plugins based on this.csPlugin, + * this.csPluginPath and the built in plugins. */ public async loadPlugins(): Promise { // Built-in plugins. diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 69c4572ee..5836deada 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -12,8 +12,7 @@ describe("plugin", () => { const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") await papi.loadPlugins() - // We remove the function fields from the application's plugins. - const apps = JSON.parse(JSON.stringify(await papi.applications())) + const apps = await papi.applications() assert.deepEqual( [ diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile new file mode 100644 index 000000000..fb66dc81a --- /dev/null +++ b/test/test-plugin/Makefile @@ -0,0 +1,5 @@ +out/index.js: src/index.ts + yarn build + +node_modules: package.json yarn.lock + yarn From 8d3a7721feaa7e319ec6182fbe4806a301b2671a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 16:42:18 -0500 Subject: [PATCH 52/82] plugin.d.ts: Document plugin priority correctly --- src/node/plugin.ts | 16 ++++++++-------- typings/plugin.d.ts | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index f0dca2754..a34c2027f 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -89,8 +89,12 @@ export class PluginAPI { * this.csPluginPath and the built in plugins. */ public async loadPlugins(): Promise { - // Built-in plugins. - await this._loadPlugins(path.join(__dirname, "../../plugins")) + for (const dir of this.csPlugin.split(":")) { + if (!dir) { + continue + } + await this.loadPlugin(dir) + } for (const dir of this.csPluginPath.split(":")) { if (!dir) { @@ -99,12 +103,8 @@ export class PluginAPI { await this._loadPlugins(dir) } - for (const dir of this.csPlugin.split(":")) { - if (!dir) { - continue - } - await this.loadPlugin(dir) - } + // Built-in plugins. + await this._loadPlugins(path.join(__dirname, "../../plugins")) } private async _loadPlugins(dir: string): Promise { diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts index 92c3acada..549c15f11 100644 --- a/typings/plugin.d.ts +++ b/typings/plugin.d.ts @@ -18,7 +18,12 @@ import * as express from "express" * * Plugins are just node modules. * - * code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in + * 1. code-server uses $CS_PLUGIN to find plugins. + * + * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load + * /tmp/will and /tmp/teffen as plugins. + * + * 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in * $CS_PLUGIN_PATH with a package.json where the engine is code-server is * a valid plugin. * @@ -29,16 +34,14 @@ import * as express from "express" * ~/.local/share/code-server/plugins:/usr/share/code-server/plugins * if unset. * - * code-server also uses $CS_PLUGIN to find plugins. * - * e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load - * /tmp/will and /tmp/teffen as plugins. + * 3. Built in plugins are loaded from __dirname/../plugins * - * Built in plugins are also loaded from __dirname/../plugins + * Plugins are required as soon as they are found and then initialized. + * See the Plugin interface for details. * - * Priority is $CS_PLUGIN, $CS_PLUGIN_PATH and then the builtin plugins. - * After the search is complete, plugins will be required in first found order and - * initialized. See the Plugin interface for details. + * If two plugins are found with the exact same name, then code-server will + * use the first one and emit a warning. * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard @@ -51,9 +54,6 @@ import * as express from "express" * The plugin's name, description and version are fetched from its module's package.json * * The plugin's router will be mounted at / - * - * If two plugins are found with the exact same name, then code-server will - * use the last one and emit a warning. */ export interface Plugin { /** From 6638daf6f05a21bf82dab86690635045443a055d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 17:09:28 -0500 Subject: [PATCH 53/82] plugin.d.ts: Add explicit path field and adjust types to reflect See my discussion with Will in the PR. --- typings/plugin.d.ts | 54 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/typings/plugin.d.ts b/typings/plugin.d.ts index 549c15f11..bc8d2ef58 100644 --- a/typings/plugin.d.ts +++ b/typings/plugin.d.ts @@ -10,7 +10,7 @@ import * as express from "express" * The homepage of code-server will launch into VS Code. However, there will be an overlay * button that when clicked, will show all available applications with their names, * icons and provider plugins. When one clicks on an app's icon, they will be directed - * to // to access the application. + * to // to access the application. */ /** @@ -51,11 +51,35 @@ import * as express from "express" /** * Your plugin module must implement this interface. * - * The plugin's name, description and version are fetched from its module's package.json - * - * The plugin's router will be mounted at / + * The plugin's router will be mounted at / */ export interface Plugin { + /** + * name is used as the plugin's unique identifier. + * No two plugins may share the same name. + * + * Fetched from package.json. + */ + name?: string + + /** + * The version for the plugin in the overlay. + * + * Fetched from package.json. + */ + version?: string + + /** + * These two are used in the overlay. + */ + displayName: string + description: string + + /** + * The path at which the plugin router is to be registered. + */ + path: string + /** * init is called so that the plugin may initialize itself with the config. */ @@ -63,6 +87,8 @@ export interface Plugin { /** * Returns the plugin's router. + * + * Mounted at / */ router(): express.Router @@ -90,21 +116,25 @@ export interface PluginConfig { /** * Application represents a user accessible application. - * - * When the user clicks on the icon in the overlay, they will be - * redirected to // - * where the application should be accessible. - * - * If the app's name is the same as the plugin's name then - * / will be used instead. */ export interface Application { readonly name: string readonly version: string + /** + * When the user clicks on the icon in the overlay, they will be + * redirected to // + * where the application should be accessible. + * + * If undefined, then / is used. + */ + readonly path?: string + + readonly description?: string + /** * The path at which the icon for this application can be accessed. - * /// + * /// */ readonly iconPath: string } From fed545e67d77e5792a0a4ee00aa4a0485ff925eb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 17:13:21 -0500 Subject: [PATCH 54/82] plugin.d.ts -> pluginapi.d.ts More clear. --- src/node/plugin.ts | 4 ++-- test/test-plugin/src/index.ts | 2 +- typings/{plugin.d.ts => pluginapi.d.ts} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename typings/{plugin.d.ts => pluginapi.d.ts} (100%) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index a34c2027f..061523a0c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -3,7 +3,7 @@ import * as express from "express" import * as fs from "fs" import * as path from "path" import * as semver from "semver" -import * as pluginapi from "../../typings/plugin" +import * as pluginapi from "../../typings/pluginapi" import { version } from "./constants" import * as util from "./util" const fsp = fs.promises @@ -30,7 +30,7 @@ interface Application extends pluginapi.Application { } /** - * PluginAPI implements the plugin API described in typings/plugin.d.ts + * PluginAPI implements the plugin API described in typings/pluginapi.d.ts * Please see that file for details. */ export class PluginAPI { diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 94bf73b80..bc37d7c05 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,6 +1,6 @@ import * as express from "express" import * as path from "path" -import * as pluginapi from "../../../typings/plugin" +import * as pluginapi from "../../../typings/pluginapi" export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") diff --git a/typings/plugin.d.ts b/typings/pluginapi.d.ts similarity index 100% rename from typings/plugin.d.ts rename to typings/pluginapi.d.ts From afff86ae9cd68f5e8d87e28dc9e78002489874df Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:11:14 -0500 Subject: [PATCH 55/82] plugin.ts: Adjust to implement pluginapi.d.ts correctly --- src/node/plugin.ts | 33 +++++++++++++++++++++++---------- test/plugin.test.ts | 10 ++++++++-- test/test-plugin/package.json | 1 - test/test-plugin/src/index.ts | 12 +++++++++--- tsconfig.json | 3 ++- typings/pluginapi.d.ts | 7 ++++++- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 061523a0c..8d5e552b5 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -10,11 +10,11 @@ const fsp = fs.promises interface Plugin extends pluginapi.Plugin { /** - * These fields are populated from the plugin's package.json. + * These fields are populated from the plugin's package.json + * and now guaranteed to exist. */ name: string version: string - description: string /** * path to the node module on the disk. @@ -34,7 +34,7 @@ interface Application extends pluginapi.Application { * Please see that file for details. */ export class PluginAPI { - private readonly plugins = new Array() + private readonly plugins = new Map() private readonly logger: Logger public constructor( @@ -54,7 +54,7 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { const pluginApps = await p.applications() // Add plugin key to each app. @@ -65,8 +65,11 @@ export class PluginAPI { plugin: { name: p.name, version: p.version, - description: p.description, modulePath: p.modulePath, + + displayName: p.displayName, + description: p.description, + path: p.path, }, } }), @@ -79,7 +82,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -129,7 +132,7 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) - for (const p of this.plugins) { + for (const [_, p] of this.plugins) { if (p.name === packageJSON.name) { this.logger.warn( `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, @@ -138,7 +141,7 @@ export class PluginAPI { } } const p = this._loadPlugin(dir, packageJSON) - this.plugins.push(p) + this.plugins.set(p.name, p) } catch (err) { if (err.code !== "ENOENT") { this.logger.warn(`failed to load plugin: ${err.message}`) @@ -147,6 +150,8 @@ export class PluginAPI { } private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { + dir = path.resolve(dir) + const logger = this.logger.named(packageJSON.name) logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) @@ -165,11 +170,20 @@ export class PluginAPI { const p = { name: packageJSON.name, version: packageJSON.version, - description: packageJSON.description, modulePath: dir, ...require(dir), } as Plugin + if (!p.displayName) { + throw new Error("plugin missing displayName") + } + if (!p.description) { + throw new Error("plugin missing description") + } + if (!p.path) { + throw new Error("plugin missing path") + } + p.init({ logger: logger, }) @@ -183,7 +197,6 @@ export class PluginAPI { interface PackageJSON { name: string version: string - description: string engines: { "code-server": string } diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 5836deada..1e63fa8d8 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -17,14 +17,20 @@ describe("plugin", () => { assert.deepEqual( [ { - name: "goland", + name: "test app", version: "4.0.0", + + description: "my description", iconPath: "/icon.svg", + plugin: { name: "test-plugin", version: "1.0.0", - description: "Fake plugin for testing code-server's plugin API", modulePath: path.join(__dirname, "test-plugin"), + + description: "Plugin used in code-server tests.", + displayName: "Test Plugin", + path: "/test-plugin", }, }, ], diff --git a/test/test-plugin/package.json b/test/test-plugin/package.json index ccdeabb56..c1f2e6980 100644 --- a/test/test-plugin/package.json +++ b/test/test-plugin/package.json @@ -2,7 +2,6 @@ "private": true, "name": "test-plugin", "version": "1.0.0", - "description": "Fake plugin for testing code-server's plugin API", "engines": { "code-server": "^3.6.0" }, diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index bc37d7c05..6435592b5 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -1,7 +1,11 @@ import * as express from "express" -import * as path from "path" +import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" +export const displayName = "Test Plugin" +export const path = "/test-plugin" +export const description = "Plugin used in code-server tests." + export function init(config: pluginapi.PluginConfig) { config.logger.debug("test-plugin loaded!") } @@ -9,7 +13,7 @@ export function init(config: pluginapi.PluginConfig) { export function router(): express.Router { const r = express.Router() r.get("/goland/icon.svg", (req, res) => { - res.sendFile(path.resolve(__dirname, "../public/icon.svg")) + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r } @@ -17,9 +21,11 @@ export function router(): express.Router { export function applications(): pluginapi.Application[] { return [ { - name: "goland", + name: "test app", version: "4.0.0", iconPath: "/icon.svg", + + description: "my description", }, ] } diff --git a/tsconfig.json b/tsconfig.json index ac3a1df52..0db0b1908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "tsBuildInfoFile": "./.cache/tsbuildinfo", "incremental": true, "rootDir": "./src", - "typeRoots": ["./node_modules/@types", "./typings"] + "typeRoots": ["./node_modules/@types", "./typings"], + "downlevelIteration": true }, "include": ["./src/**/*.ts"] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index bc8d2ef58..7b61c4cef 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -70,9 +70,14 @@ export interface Plugin { version?: string /** - * These two are used in the overlay. + * Name used in the overlay. */ displayName: string + + /** + * Used in overlay. + * Should be a full sentence describing the plugin. + */ description: string /** From e03bbe31497b5581e578e03ebc5abc7a11b43580 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:14:14 -0500 Subject: [PATCH 56/82] routes/apps.ts: Implement /api/applications endpoint --- src/node/routes/apps.ts | 12 ++++++++++++ src/node/routes/index.ts | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 src/node/routes/apps.ts diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts new file mode 100644 index 000000000..970bd3cb1 --- /dev/null +++ b/src/node/routes/apps.ts @@ -0,0 +1,12 @@ +import * as express from "express" +import { PluginAPI } from "../plugin" + +export function router(papi: PluginAPI): express.Router { + const router = express.Router() + + router.get("/", async (req, res) => { + res.json(await papi.applications()) + }) + + return router +} diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 5824475d9..a39b2a6a1 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -23,6 +23,7 @@ import * as proxy from "./pathProxy" import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" +import * as apps from "./apps" declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -118,6 +119,7 @@ export const register = async ( const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH) await papi.loadPlugins() papi.mount(app) + app.use("/api/applications", apps.router(papi)) app.use(() => { throw new HttpError("Not Found", HttpCode.NotFound) From 139a28e0ea063120a6adc4880f93f9c4d14bbdd4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:14:19 -0500 Subject: [PATCH 57/82] plugin.ts: Describe private counterpart functions Addresses Will's comments. --- src/node/plugin.ts | 12 ++++++++++++ src/node/routes/apps.ts | 3 +++ test/plugin.test.ts | 6 +++--- test/test-plugin/src/index.ts | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 8d5e552b5..ce424770b 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -110,6 +110,13 @@ export class PluginAPI { await this._loadPlugins(path.join(__dirname, "../../plugins")) } + /** + * _loadPlugins is the counterpart to loadPlugins. + * + * It differs in that it loads all plugins in a single + * directory whereas loadPlugins uses all available directories + * as documented. + */ private async _loadPlugins(dir: string): Promise { try { const entries = await fsp.readdir(dir, { withFileTypes: true }) @@ -149,6 +156,11 @@ export class PluginAPI { } } + /** + * _loadPlugin is the counterpart to loadPlugin and actually + * loads the plugin now that we know there is no duplicate + * and that the package.json has been read. + */ private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin { dir = path.resolve(dir) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index 970bd3cb1..c678f2fee 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -1,6 +1,9 @@ import * as express from "express" import { PluginAPI } from "../plugin" +/** + * Implements the /api/applications endpoint + */ export function router(papi: PluginAPI): express.Router { const router = express.Router() diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 1e63fa8d8..8c419139a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -17,10 +17,10 @@ describe("plugin", () => { assert.deepEqual( [ { - name: "test app", + name: "Test App", version: "4.0.0", - description: "my description", + description: "This app does XYZ.", iconPath: "/icon.svg", plugin: { @@ -28,8 +28,8 @@ describe("plugin", () => { version: "1.0.0", modulePath: path.join(__dirname, "test-plugin"), - description: "Plugin used in code-server tests.", displayName: "Test Plugin", + description: "Plugin used in code-server tests.", path: "/test-plugin", }, }, diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 6435592b5..f9f316b4e 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -21,11 +21,11 @@ export function router(): express.Router { export function applications(): pluginapi.Application[] { return [ { - name: "test app", + name: "Test App", version: "4.0.0", iconPath: "/icon.svg", - description: "my description", + description: "This app does XYZ.", }, ] } From 687094802ec4bb88ed4b07ecdf072e3249dfc7ca Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:42:21 -0500 Subject: [PATCH 58/82] plugin.ts: Make application endpoint paths absolute --- src/node/plugin.ts | 8 +++++--- test/plugin.test.ts | 5 +++-- test/test-plugin/Makefile | 3 ++- test/test-plugin/src/index.ts | 3 ++- typings/pluginapi.d.ts | 14 ++++++++------ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index ce424770b..9523782bc 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -60,6 +60,8 @@ export class PluginAPI { // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { + app = {...app, path: path.join(p.routerPath, app.path || "")} + app = {...app, iconPath: path.join(app.path || "", app.iconPath)} return { ...app, plugin: { @@ -69,7 +71,7 @@ export class PluginAPI { displayName: p.displayName, description: p.description, - path: p.path, + routerPath: p.routerPath, }, } }), @@ -192,8 +194,8 @@ export class PluginAPI { if (!p.description) { throw new Error("plugin missing description") } - if (!p.path) { - throw new Error("plugin missing path") + if (!p.routerPath) { + throw new Error("plugin missing router path") } p.init({ diff --git a/test/plugin.test.ts b/test/plugin.test.ts index 8c419139a..bc13fc803 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -21,7 +21,8 @@ describe("plugin", () => { version: "4.0.0", description: "This app does XYZ.", - iconPath: "/icon.svg", + iconPath: "/test-plugin/test-app/icon.svg", + path: "/test-plugin/test-app", plugin: { name: "test-plugin", @@ -30,7 +31,7 @@ describe("plugin", () => { displayName: "Test Plugin", description: "Plugin used in code-server tests.", - path: "/test-plugin", + routerPath: "/test-plugin", }, }, ], diff --git a/test/test-plugin/Makefile b/test/test-plugin/Makefile index fb66dc81a..d01aa80a8 100644 --- a/test/test-plugin/Makefile +++ b/test/test-plugin/Makefile @@ -1,5 +1,6 @@ out/index.js: src/index.ts - yarn build + # Typescript always emits, even on errors. + yarn build || rm out/index.js node_modules: package.json yarn.lock yarn diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index f9f316b4e..161203319 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -3,7 +3,7 @@ import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" export const displayName = "Test Plugin" -export const path = "/test-plugin" +export const routerPath = "/test-plugin" export const description = "Plugin used in code-server tests." export function init(config: pluginapi.PluginConfig) { @@ -24,6 +24,7 @@ export function applications(): pluginapi.Application[] { name: "Test App", version: "4.0.0", iconPath: "/icon.svg", + path: "/test-app", description: "This app does XYZ.", }, diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 7b61c4cef..dbb985a58 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -45,7 +45,9 @@ import * as express from "express" * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard - * for example. + * for example. An important difference with the API is that all application paths + * will be absolute (i.e have the plugin path prepended) so that they may be used + * directly. */ /** @@ -60,30 +62,30 @@ export interface Plugin { * * Fetched from package.json. */ - name?: string + readonly name?: string /** * The version for the plugin in the overlay. * * Fetched from package.json. */ - version?: string + readonly version?: string /** * Name used in the overlay. */ - displayName: string + readonly displayName: string /** * Used in overlay. * Should be a full sentence describing the plugin. */ - description: string + readonly description: string /** * The path at which the plugin router is to be registered. */ - path: string + readonly routerPath: string /** * init is called so that the plugin may initialize itself with the config. From 2a13d003d37d97f8650bd15f29baa45cd4fb1d5a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:45:25 -0500 Subject: [PATCH 59/82] plugin.ts: Add homepageURL to plugin and application --- src/node/plugin.ts | 4 ++++ test/plugin.test.ts | 2 ++ test/test-plugin/src/index.ts | 2 ++ typings/pluginapi.d.ts | 10 ++++++++++ 4 files changed, 18 insertions(+) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 9523782bc..368d6f7ff 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -72,6 +72,7 @@ export class PluginAPI { displayName: p.displayName, description: p.description, routerPath: p.routerPath, + homepageURL: p.homepageURL, }, } }), @@ -197,6 +198,9 @@ export class PluginAPI { if (!p.routerPath) { throw new Error("plugin missing router path") } + if (!p.homepageURL) { + throw new Error("plugin missing homepage") + } p.init({ logger: logger, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index bc13fc803..ed040dc21 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -22,6 +22,7 @@ describe("plugin", () => { description: "This app does XYZ.", iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", path: "/test-plugin/test-app", plugin: { @@ -32,6 +33,7 @@ describe("plugin", () => { displayName: "Test Plugin", description: "Plugin used in code-server tests.", routerPath: "/test-plugin", + homepageURL: "https://example.com", }, }, ], diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 161203319..2fc1ddab0 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -4,6 +4,7 @@ import * as pluginapi from "../../../typings/pluginapi" export const displayName = "Test Plugin" export const routerPath = "/test-plugin" +export const homepageURL = "https://example.com" export const description = "Plugin used in code-server tests." export function init(config: pluginapi.PluginConfig) { @@ -27,6 +28,7 @@ export function applications(): pluginapi.Application[] { path: "/test-app", description: "This app does XYZ.", + homepageURL: "https://example.com", }, ] } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index dbb985a58..3ce5e5d0c 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -87,6 +87,11 @@ export interface Plugin { */ readonly routerPath: string + /** + * Link to plugin homepage. + */ + readonly homepageURL: string + /** * init is called so that the plugin may initialize itself with the config. */ @@ -144,4 +149,9 @@ export interface Application { * /// */ readonly iconPath: string + + /** + * Link to application homepage. + */ + readonly homepageURL: string } From af73b96313c4d31da917ec46cbee390c7bbb32e9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:49:10 -0500 Subject: [PATCH 60/82] routes/apps.ts: Add example output --- src/node/routes/apps.ts | 2 ++ typings/pluginapi.d.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index c678f2fee..4298fb392 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -3,6 +3,8 @@ import { PluginAPI } from "../plugin" /** * Implements the /api/applications endpoint + * + * See typings/pluginapi.d.ts for details. */ export function router(papi: PluginAPI): express.Router { const router = express.Router() diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 3ce5e5d0c..94819bd3c 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -42,12 +42,38 @@ import * as express from "express" * * If two plugins are found with the exact same name, then code-server will * use the first one and emit a warning. + * + */ + +/* Programmability * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard * for example. An important difference with the API is that all application paths * will be absolute (i.e have the plugin path prepended) so that they may be used * directly. + * + * Example output: + * + * [ + * { + * "name": "Test App", + * "version": "4.0.0", + * "iconPath": "/test-plugin/test-app/icon.svg", + * "path": "/test-plugin/test-app", + * "description": "This app does XYZ.", + * "homepageURL": "https://example.com", + * "plugin": { + * "name": "test-plugin", + * "version": "1.0.0", + * "modulePath": "/Users/nhooyr/src/cdr/code-server/test/test-plugin", + * "displayName": "Test Plugin", + * "description": "Plugin used in code-server tests.", + * "routerPath": "/test-plugin", + * "homepageURL": "https://example.com" + * } + * } + * ] */ /** From 706bc23f0489bf0fe9a4b844cc36077f52600041 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Nov 2020 21:53:16 -0500 Subject: [PATCH 61/82] plugin: Fixes for CI --- ci/dev/test.sh | 2 +- src/node/plugin.ts | 10 +++++----- src/node/routes/apps.ts | 2 +- src/node/routes/index.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 6eaa3878d..9922a9c84 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -6,7 +6,7 @@ main() { cd test/test-plugin make -s out/index.js - cd $OLDPWD + cd "$OLDPWD" mocha -r ts-node/register ./test/*.test.ts "$@" } diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 368d6f7ff..fea85710c 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -54,14 +54,14 @@ export class PluginAPI { */ public async applications(): Promise { const apps = new Array() - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { const pluginApps = await p.applications() // Add plugin key to each app. apps.push( ...pluginApps.map((app) => { - app = {...app, path: path.join(p.routerPath, app.path || "")} - app = {...app, iconPath: path.join(app.path || "", app.iconPath)} + app = { ...app, path: path.join(p.routerPath, app.path || "") } + app = { ...app, iconPath: path.join(app.path || "", app.iconPath) } return { ...app, plugin: { @@ -85,7 +85,7 @@ export class PluginAPI { * mount mounts all plugin routers onto r. */ public mount(r: express.Router): void { - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { r.use(`/${p.name}`, p.router()) } } @@ -142,7 +142,7 @@ export class PluginAPI { encoding: "utf8", }) const packageJSON: PackageJSON = JSON.parse(str) - for (const [_, p] of this.plugins) { + for (const [, p] of this.plugins) { if (p.name === packageJSON.name) { this.logger.warn( `ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`, diff --git a/src/node/routes/apps.ts b/src/node/routes/apps.ts index 4298fb392..5c8541fc9 100644 --- a/src/node/routes/apps.ts +++ b/src/node/routes/apps.ts @@ -12,6 +12,6 @@ export function router(papi: PluginAPI): express.Router { router.get("/", async (req, res) => { res.json(await papi.applications()) }) - + return router } diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index a39b2a6a1..da714eea5 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -16,6 +16,7 @@ import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" import * as domainProxy from "./domainProxy" +import * as apps from "./apps" import * as health from "./health" import * as login from "./login" import * as proxy from "./pathProxy" @@ -23,7 +24,6 @@ import * as proxy from "./pathProxy" import * as _static from "./static" import * as update from "./update" import * as vscode from "./vscode" -import * as apps from "./apps" declare global { // eslint-disable-next-line @typescript-eslint/no-namespace From 8a8159c683c07c8f8e854b7c176077e1b8360c39 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Nov 2020 22:59:43 -0500 Subject: [PATCH 62/82] plugin: More review fixes Next commit will address Will's comments about the typings being weird. --- src/node/plugin.ts | 26 ++++++++++++++++++-------- typings/pluginapi.d.ts | 3 ++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index fea85710c..0ac3abfc8 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -86,7 +86,7 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { - r.use(`/${p.name}`, p.router()) + r.use(`/${p.routerPath}`, p.router()) } } @@ -154,7 +154,7 @@ export class PluginAPI { this.plugins.set(p.name, p) } catch (err) { if (err.code !== "ENOENT") { - this.logger.warn(`failed to load plugin: ${err.message}`) + this.logger.warn(`failed to load plugin: ${err.stack}`) } } } @@ -170,17 +170,24 @@ export class PluginAPI { const logger = this.logger.named(packageJSON.name) logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON)) + if (!packageJSON.name) { + throw new Error("plugin package.json missing name") + } + if (!packageJSON.version) { + throw new Error("plugin package.json missing version") + } + if (!packageJSON.engines || !packageJSON.engines["code-server"]) { + throw new Error(`plugin package.json missing code-server range like: + "engines": { + "code-server": "^3.6.0" + } +`) + } if (!semver.satisfies(version, packageJSON.engines["code-server"])) { throw new Error( `plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`, ) } - if (!packageJSON.name) { - throw new Error("plugin missing name") - } - if (!packageJSON.version) { - throw new Error("plugin missing version") - } const p = { name: packageJSON.name, @@ -198,6 +205,9 @@ export class PluginAPI { if (!p.routerPath) { throw new Error("plugin missing router path") } + if (!p.routerPath.startsWith("/") || p.routerPath.length < 2) { + throw new Error(`plugin router path ${q(p.routerPath)}: invalid`) + } if (!p.homepageURL) { throw new Error("plugin missing homepage") } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 94819bd3c..4e3971eeb 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -45,7 +45,8 @@ import * as express from "express" * */ -/* Programmability +/** + * Programmability * * There is also a /api/applications endpoint to allow programmatic access to all * available applications. It could be used to create a custom application dashboard From 14f408a837cbdd301545d4241da4e6ecd90857cb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Nov 2020 23:10:41 -0500 Subject: [PATCH 63/82] plugin: Plugin modules now export a single top level identifier Makes typing much easier. Addresse's Will's last comment. --- src/node/plugin.ts | 7 ++++- test/test-plugin/src/index.ts | 58 ++++++++++++++++++----------------- typings/pluginapi.d.ts | 5 +-- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 0ac3abfc8..71831f504 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -189,11 +189,16 @@ export class PluginAPI { ) } + const pluginModule = require(dir) + if (!pluginModule.plugin) { + throw new Error("plugin module does not export a plugin") + } + const p = { name: packageJSON.name, version: packageJSON.version, modulePath: dir, - ...require(dir), + ...pluginModule.plugin, } as Plugin if (!p.displayName) { diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 2fc1ddab0..2b00c2ec5 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -2,33 +2,35 @@ import * as express from "express" import * as fspath from "path" import * as pluginapi from "../../../typings/pluginapi" -export const displayName = "Test Plugin" -export const routerPath = "/test-plugin" -export const homepageURL = "https://example.com" -export const description = "Plugin used in code-server tests." +export const plugin: pluginapi.Plugin = { + displayName: "Test Plugin", + routerPath: "/test-plugin", + homepageURL: "https://example.com", + description: "Plugin used in code-server tests.", -export function init(config: pluginapi.PluginConfig) { - config.logger.debug("test-plugin loaded!") -} - -export function router(): express.Router { - const r = express.Router() - r.get("/goland/icon.svg", (req, res) => { - res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) - }) - return r -} - -export function applications(): pluginapi.Application[] { - return [ - { - name: "Test App", - version: "4.0.0", - iconPath: "/icon.svg", - path: "/test-app", - - description: "This app does XYZ.", - homepageURL: "https://example.com", - }, - ] + init: (config) => { + config.logger.debug("test-plugin loaded!") + }, + + router: () => { + const r = express.Router() + r.get("/goland/icon.svg", (req, res) => { + res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) + }) + return r + }, + + applications: () => { + return [ + { + name: "Test App", + version: "4.0.0", + iconPath: "/icon.svg", + path: "/test-app", + + description: "This app does XYZ.", + homepageURL: "https://example.com", + }, + ] + }, } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index 4e3971eeb..d0846a288 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -16,7 +16,8 @@ import * as express from "express" /** * Plugins * - * Plugins are just node modules. + * Plugins are just node modules that contain a top level export "plugin" that implements + * the Plugin interface. * * 1. code-server uses $CS_PLUGIN to find plugins. * @@ -78,7 +79,7 @@ import * as express from "express" */ /** - * Your plugin module must implement this interface. + * Your plugin module must have a top level export "plugin" that implements this interface. * * The plugin's router will be mounted at / */ From 9453f891df6283747f0b53b7dfb52524bdf346ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 5 Nov 2020 14:17:13 -0500 Subject: [PATCH 64/82] plugin.ts: Fix usage of routerPath in mount --- src/node/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 71831f504..77a4a8277 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -86,7 +86,7 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { - r.use(`/${p.routerPath}`, p.router()) + r.use(`${p.routerPath}`, p.router()) } } From 197a09f0c1227a47ea709042ea6c46bb6ded5227 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 09:51:46 -0500 Subject: [PATCH 65/82] plugin: Test endpoints via supertest Unfortunately we can't use node-mocks-http to test a express.Router that has async routes. See https://github.com/howardabrams/node-mocks-http/issues/225 router will just return undefined if the executing handler is async and so the test will have no way to wait for it to complete. Thus, we have to use supertest which starts an actual HTTP server in the background and uses a HTTP client to send requests. --- package.json | 2 + test/plugin.test.ts | 66 ++++++++++++++++----------- test/test-plugin/src/index.ts | 11 +++-- yarn.lock | 85 +++++++++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 6459e6a0f..c41b8b41c 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", "@types/split2": "^2.1.6", + "@types/supertest": "^2.0.10", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", @@ -59,6 +60,7 @@ "prettier": "^2.0.5", "stylelint": "^13.0.0", "stylelint-config-recommended": "^3.0.0", + "supertest": "^6.0.1", "ts-node": "^9.0.0", "typescript": "4.0.2" }, diff --git a/test/plugin.test.ts b/test/plugin.test.ts index ed040dc21..aaf8c14dc 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,43 +1,57 @@ import { logger } from "@coder/logger" -import * as assert from "assert" import { describe } from "mocha" import * as path from "path" import { PluginAPI } from "../src/node/plugin" +import * as supertest from "supertest" +import * as express from "express" +import * as apps from "../src/node/routes/apps" /** * Use $LOG_LEVEL=debug to see debug logs. */ describe("plugin", () => { - it("loads", async () => { - const papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") + let papi: PluginAPI + let app: express.Application + let agent: supertest.SuperAgentTest + + before(async () => { + papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow") await papi.loadPlugins() - const apps = await papi.applications() + app = express.default() + papi.mount(app) - assert.deepEqual( - [ - { - name: "Test App", - version: "4.0.0", + app.use("/api/applications", apps.router(papi)) - description: "This app does XYZ.", - iconPath: "/test-plugin/test-app/icon.svg", + agent = supertest.agent(app) + }) + + it("/api/applications", async () => { + await agent.get("/api/applications").expect(200, [ + { + name: "Test App", + version: "4.0.0", + + description: "This app does XYZ.", + iconPath: "/test-plugin/test-app/icon.svg", + homepageURL: "https://example.com", + path: "/test-plugin/test-app", + + plugin: { + name: "test-plugin", + version: "1.0.0", + modulePath: path.join(__dirname, "test-plugin"), + + displayName: "Test Plugin", + description: "Plugin used in code-server tests.", + routerPath: "/test-plugin", homepageURL: "https://example.com", - path: "/test-plugin/test-app", - - plugin: { - name: "test-plugin", - version: "1.0.0", - modulePath: path.join(__dirname, "test-plugin"), - - displayName: "Test Plugin", - description: "Plugin used in code-server tests.", - routerPath: "/test-plugin", - homepageURL: "https://example.com", - }, }, - ], - apps, - ) + }, + ]) + }) + + it("/test-plugin/test-app", async () => { + await agent.get("/test-plugin/test-app").expect(200, { date: "2000-02-05T05:00:00.000Z" }) }) }) diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 2b00c2ec5..9e95ffca7 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -8,19 +8,24 @@ export const plugin: pluginapi.Plugin = { homepageURL: "https://example.com", description: "Plugin used in code-server tests.", - init: (config) => { + init(config) { config.logger.debug("test-plugin loaded!") }, - router: () => { + router() { const r = express.Router() + r.get("/test-app", (req, res) => { + res.json({ + date: new Date("2000/02/05"), + }) + }) r.get("/goland/icon.svg", (req, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) }) return r }, - applications: () => { + applications() { return [ { name: "Test App", diff --git a/yarn.lock b/yarn.lock index d96b2e6ee..034efecce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1035,6 +1035,11 @@ dependencies: "@types/express" "*" +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1172,6 +1177,21 @@ dependencies: "@types/node" "*" +"@types/superagent@*": + version "4.1.10" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.10.tgz#5e2cc721edf58f64fe9b819f326ee74803adee86" + integrity sha512-xAgkb2CMWUMCyVc/3+7iQfOEBE75NvuZeezvmixbUw3nmENf2tCnQkW5yQLTYqvXUQ+R6EXxdqKKbal2zM5V/g== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.10.tgz#630d79b4d82c73e043e43ff777a9ca98d457cab7" + integrity sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ== + dependencies: + "@types/superagent" "*" + "@types/tar-fs@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1" @@ -2182,7 +2202,7 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2204,7 +2224,7 @@ commander@^5.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -component-emitter@^1.2.1: +component-emitter@^1.2.1, component-emitter@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== @@ -2276,6 +2296,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookiejar@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -3351,6 +3376,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -3493,6 +3523,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -3507,6 +3546,11 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -4807,7 +4851,7 @@ merge2@^1.2.3, merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@1.1.2, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -4864,6 +4908,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -6181,6 +6230,11 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -7246,6 +7300,31 @@ sugarss@^2.0.0: dependencies: postcss "^7.0.2" +superagent@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supertest@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.0.1.tgz#f6b54370de85c45d6557192c8d7df604ca2c9e18" + integrity sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g== + dependencies: + methods "1.1.2" + superagent "6.1.0" + supports-color@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" From 9d39c53c99eb5398ecd0df5a3ef0c0f8b3ec80c7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 10:09:35 -0500 Subject: [PATCH 66/82] plugin: Give test-plugin some html to test overlay --- test/plugin.test.ts | 7 ++++++- test/test-plugin/public/index.html | 10 ++++++++++ test/test-plugin/src/index.ts | 4 +--- 3 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 test/test-plugin/public/index.html diff --git a/test/plugin.test.ts b/test/plugin.test.ts index aaf8c14dc..a0885916b 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -5,6 +5,8 @@ import { PluginAPI } from "../src/node/plugin" import * as supertest from "supertest" import * as express from "express" import * as apps from "../src/node/routes/apps" +import * as fs from "fs" +const fsp = fs.promises /** * Use $LOG_LEVEL=debug to see debug logs. @@ -52,6 +54,9 @@ describe("plugin", () => { }) it("/test-plugin/test-app", async () => { - await agent.get("/test-plugin/test-app").expect(200, { date: "2000-02-05T05:00:00.000Z" }) + const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), { + encoding: "utf8", + }) + await agent.get("/test-plugin/test-app").expect(200, indexHTML) }) }) diff --git a/test/test-plugin/public/index.html b/test/test-plugin/public/index.html new file mode 100644 index 000000000..3485f18e5 --- /dev/null +++ b/test/test-plugin/public/index.html @@ -0,0 +1,10 @@ + + + + + Test Plugin + + +

Welcome to the test plugin!

+ + diff --git a/test/test-plugin/src/index.ts b/test/test-plugin/src/index.ts index 9e95ffca7..fb1869447 100644 --- a/test/test-plugin/src/index.ts +++ b/test/test-plugin/src/index.ts @@ -15,9 +15,7 @@ export const plugin: pluginapi.Plugin = { router() { const r = express.Router() r.get("/test-app", (req, res) => { - res.json({ - date: new Date("2000/02/05"), - }) + res.sendFile(fspath.resolve(__dirname, "../public/index.html")) }) r.get("/goland/icon.svg", (req, res) => { res.sendFile(fspath.resolve(__dirname, "../public/icon.svg")) From 277211c4ce0ca66af19c0ffb80a4d64dec4099b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 14:44:19 -0500 Subject: [PATCH 67/82] plugin: Make init and applications callbacks optional --- src/node/plugin.ts | 6 ++++++ typings/pluginapi.d.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 77a4a8277..899aa1111 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -55,6 +55,9 @@ export class PluginAPI { public async applications(): Promise { const apps = new Array() for (const [, p] of this.plugins) { + if (!p.applications) { + continue + } const pluginApps = await p.applications() // Add plugin key to each app. @@ -86,6 +89,9 @@ export class PluginAPI { */ public mount(r: express.Router): void { for (const [, p] of this.plugins) { + if (!p.router) { + continue + } r.use(`${p.routerPath}`, p.router()) } } diff --git a/typings/pluginapi.d.ts b/typings/pluginapi.d.ts index d0846a288..06ce35fb4 100644 --- a/typings/pluginapi.d.ts +++ b/typings/pluginapi.d.ts @@ -129,8 +129,10 @@ export interface Plugin { * Returns the plugin's router. * * Mounted at / + * + * If not present, the plugin provides no routes. */ - router(): express.Router + router?(): express.Router /** * code-server uses this to collect the list of applications that @@ -139,8 +141,10 @@ export interface Plugin { * refresh the list of applications * * Ensure this is as fast as possible. + * + * If not present, the plugin provides no applications. */ - applications(): Application[] | Promise + applications?(): Application[] | Promise } /** From fe399ff0fe875aed3327cbe6708572367a135f33 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Nov 2020 14:46:49 -0500 Subject: [PATCH 68/82] Fix formatting --- src/node/routes/index.ts | 2 +- test/plugin.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index da714eea5..4d92c365f 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -15,8 +15,8 @@ import { replaceTemplates } from "../http" import { PluginAPI } from "../plugin" import { getMediaMime, paths } from "../util" import { WebsocketRequest } from "../wsRouter" -import * as domainProxy from "./domainProxy" import * as apps from "./apps" +import * as domainProxy from "./domainProxy" import * as health from "./health" import * as login from "./login" import * as proxy from "./pathProxy" diff --git a/test/plugin.test.ts b/test/plugin.test.ts index a0885916b..305cf041a 100644 --- a/test/plugin.test.ts +++ b/test/plugin.test.ts @@ -1,11 +1,11 @@ import { logger } from "@coder/logger" +import * as express from "express" +import * as fs from "fs" import { describe } from "mocha" import * as path from "path" -import { PluginAPI } from "../src/node/plugin" import * as supertest from "supertest" -import * as express from "express" +import { PluginAPI } from "../src/node/plugin" import * as apps from "../src/node/routes/apps" -import * as fs from "fs" const fsp = fs.promises /** From 0a01338edd3a618a943567b8b95c7fce37890f9c Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 15:46:53 -0600 Subject: [PATCH 69/82] Deduplicate child process message dance --- src/node/vscode.ts | 116 +++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index aed005f9f..92d749447 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -13,6 +13,7 @@ export class VscodeProvider { public readonly serverRootPath: string public readonly vsRootPath: string private _vscode?: Promise + private timeoutInterval = 10000 // 10s, matches VS Code's timeouts. public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") @@ -52,33 +53,25 @@ export class VscodeProvider { const vscode = await this.fork() logger.debug("setting up vs code...") - return new Promise((resolve, reject) => { - const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) - vscode.once("message", (message: ipc.VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - vscode.off("error", reject) - vscode.off("exit", onExit) - return message.type === "options" && message.id === id - ? resolve(message.options) - : reject(new Error("Unexpected response during initialization")) - }) - - vscode.once("error", reject) - vscode.once("exit", onExit) - - this.send( - { - type: "init", - id, - options: { - ...options, - startPath, - }, + this.send( + { + type: "init", + id, + options: { + ...options, + startPath, }, - vscode, - ) + }, + vscode, + ) + + const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { + // There can be parallel initializations so wait for the right ID. + return message.type === "options" && message.id === id }) + + return message.options } private fork(): Promise { @@ -88,34 +81,73 @@ export class VscodeProvider { logger.debug("forking vs code...") const vscode = cp.fork(path.join(this.serverRootPath, "fork")) - vscode.on("error", (error) => { - logger.error(error.message) + + const dispose = () => { + vscode.removeAllListeners() + vscode.kill() this._vscode = undefined + } + + vscode.on("error", (error: Error) => { + logger.error(error.message) + if (error.stack) { + logger.debug(error.stack) + } + dispose() }) + vscode.on("exit", (code) => { logger.error(`VS Code exited unexpectedly with code ${code}`) - this._vscode = undefined + dispose() }) - this._vscode = new Promise((resolve, reject) => { - const onExit = (code: number | null) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)) - - vscode.once("message", (message: ipc.VscodeMessage) => { - logger.debug("got message from vs code", field("message", message)) - vscode.off("error", reject) - vscode.off("exit", onExit) - return message.type === "ready" - ? resolve(vscode) - : reject(new Error("Unexpected response waiting for ready response")) - }) - - vscode.once("error", reject) - vscode.once("exit", onExit) - }) + this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { + return message.type === "ready" + }).then(() => vscode) return this._vscode } + /** + * Listen to a single message from a process. Reject if the process errors, + * exits, or times out. + * + * `fn` is a function that determines whether the message is the one we're + * waiting for. + */ + private onMessage( + proc: cp.ChildProcess, + fn: (message: ipc.VscodeMessage) => message is T, + ): Promise { + return new Promise((resolve, _reject) => { + const reject = (error: Error) => { + clearTimeout(timeout) + _reject(error) + } + + const onExit = (code: number | null) => { + reject(new Error(`VS Code exited unexpectedly with code ${code}`)) + } + + const timeout = setTimeout(() => { + reject(new Error("timed out")) + }, this.timeoutInterval) + + proc.on("message", (message: ipc.VscodeMessage) => { + logger.debug("got message from vscode", field("message", message)) + proc.off("error", reject) + proc.off("exit", onExit) + if (fn(message)) { + clearTimeout(timeout) + resolve(message) + } + }) + + proc.once("error", reject) + proc.once("exit", onExit) + }) + } + /** * VS Code expects a raw socket. It will handle all the web socket frames. */ From de4949571cd4873116f8243a4e15d74a6b01c309 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 17:02:39 -0600 Subject: [PATCH 70/82] Document getFirstPath better --- src/node/vscode.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 92d749447..da7d5710a 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -165,7 +165,16 @@ export class VscodeProvider { } /** - * Choose the first non-empty path. + * Choose the first non-empty path from the provided array. + * + * Each array item consists of `url` and an optional `workspace` boolean that + * indicates whether that url is for a workspace. + * + * `url` can be a fully qualified URL or just the path portion. + * + * `url` can also be a query object to make it easier to pass in query + * variables directly but anything that isn't a string or string array is not + * valid and will be ignored. */ private async getFirstPath( startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, From f706039a9d4321d79cb76ff9f813f46e7e9bb8bf Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 17:24:07 -0600 Subject: [PATCH 71/82] Re-add TLS socket proxy --- src/node/vscode.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index da7d5710a..9ba7b8a90 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -6,6 +6,7 @@ import * as ipc from "../../lib/vscode/src/vs/server/ipc" import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" +import { SocketProxyProvider } from "./socket" import { isFile } from "./util" import { ipcMain } from "./wrapper" @@ -14,6 +15,7 @@ export class VscodeProvider { public readonly vsRootPath: string private _vscode?: Promise private timeoutInterval = 10000 // 10s, matches VS Code's timeouts. + private readonly socketProvider = new SocketProxyProvider() public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") @@ -22,6 +24,7 @@ export class VscodeProvider { } public async dispose(): Promise { + this.socketProvider.stop() if (this._vscode) { const vscode = await this._vscode vscode.removeAllListeners() @@ -152,9 +155,11 @@ export class VscodeProvider { * VS Code expects a raw socket. It will handle all the web socket frames. */ public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise { - // TODO: TLS socket proxy. const vscode = await this._vscode - this.send({ type: "socket", query }, vscode, socket) + // TLS sockets cannot be transferred to child processes so we need an + // in-between. Non-TLS sockets will be returned as-is. + const socketProxy = await this.socketProvider.createProxy(socket) + this.send({ type: "socket", query }, vscode, socketProxy) } private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { From b8340a2ae9f9a543fe3a34c00e51fe925bf20ecd Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 17:52:02 -0600 Subject: [PATCH 72/82] Close sockets correctly --- src/node/routes/index.ts | 2 +- src/node/wsRouter.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index 4d92c365f..2c54917da 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -155,7 +155,7 @@ export const register = async ( const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => { logger.error(`${err.message} ${err.stack}`) - ;(req as WebsocketRequest).ws.destroy(err) + ;(req as WebsocketRequest).ws.end() } wsApp.use(wsErrorHandler) diff --git a/src/node/wsRouter.ts b/src/node/wsRouter.ts index 1a057f0fa..175f214a8 100644 --- a/src/node/wsRouter.ts +++ b/src/node/wsRouter.ts @@ -5,8 +5,6 @@ import * as net from "net" export const handleUpgrade = (app: express.Express, server: http.Server): void => { server.on("upgrade", (req, socket, head) => { - socket.on("error", () => socket.destroy()) - req.ws = socket req.head = head req._ws_handled = false @@ -14,7 +12,7 @@ export const handleUpgrade = (app: express.Express, server: http.Server): void = // Send the request off to be handled by Express. ;(app as any).handle(req, new http.ServerResponse(req), () => { if (!req._ws_handled) { - socket.destroy(new Error("Not found")) + socket.end("HTTP/1.1 404 Not Found\r\n\r\n") } }) }) From 71850e312bc981a62af80f96d64c4fef39706b62 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 18:14:18 -0600 Subject: [PATCH 73/82] Avoid setting ?to=/ That's the default so it's extra visual noise. --- src/node/routes/domainProxy.ts | 2 +- src/node/routes/pathProxy.ts | 7 ++++--- src/node/routes/vscode.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index ac249b809..fdffdc585 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -55,7 +55,7 @@ router.all("*", (req, res, next) => { } // Redirect all other pages to the login. return redirect(req, res, "login", { - to: req.path, + to: req.path !== "/" ? req.path : undefined, }) } diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index d21d08eb8..152402195 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -1,6 +1,7 @@ import { Request, Router } from "express" import qs from "qs" import { HttpCode, HttpError } from "../../common/http" +import { normalize } from "../../common/util" import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -17,11 +18,11 @@ const getProxyTarget = (req: Request, rewrite: boolean): string => { router.all("/(:port)(/*)?", (req, res) => { if (!authenticated(req)) { - // If visiting the root (/proxy/:port and nothing else) redirect to the - // login page. + // If visiting the root (/:port only) redirect to the login page. if (!req.params[0] || req.params[0] === "/") { + const to = normalize(`${req.baseUrl}${req.path}`) return redirect(req, res, "login", { - to: `${req.baseUrl}${req.path}` || "/", + to: to !== "/" ? to : undefined, }) } throw new HttpError("Unauthorized", HttpCode.Unauthorized) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index db2dc2071..552001273 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -15,7 +15,8 @@ const vscode = new VscodeProvider() router.get("/", async (req, res) => { if (!authenticated(req)) { return redirect(req, res, "login", { - to: req.baseUrl, + // req.baseUrl can be blank if already at the root. + to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined, }) } From 4574593664cc4aaab167ef477389b5e88f5baa8f Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 18:20:44 -0600 Subject: [PATCH 74/82] Refactor vscode init to use async Hopefully is a bit easier to read. --- src/node/routes/vscode.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/node/routes/vscode.ts b/src/node/routes/vscode.ts index 552001273..9b464f61e 100644 --- a/src/node/routes/vscode.ts +++ b/src/node/routes/vscode.ts @@ -22,18 +22,14 @@ router.get("/", async (req, res) => { const [content, options] = await Promise.all([ await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"), - vscode - .initialize( - { - args: req.args, - remoteAuthority: req.headers.host || "", - }, - req.query, - ) - .catch((error) => { + (async () => { + 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." : "" throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`) - }), + } + })(), ]) options.productConfiguration.codeServerVersion = version From 79478eb89f30752d96ec8726dae8d359e13d369b Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 10 Nov 2020 18:33:16 -0600 Subject: [PATCH 75/82] Clarify some points around the cookie domain Also add a check that the domain has a dot. This covers the localhost case as well, so remove that. --- src/node/http.ts | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/node/http.ts b/src/node/http.ts index f259d1037..1aa7adb51 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -98,27 +98,36 @@ export const redirect = ( /** * Get the value that should be used for setting a cookie domain. This will - * allow the user to authenticate only once. This will use the highest level - * domain (e.g. `coder.com` over `test.coder.com` if both are specified). + * allow the user to authenticate once no matter what sub-domain they use to log + * in. This will use the highest level proxy domain (e.g. `coder.com` over + * `test.coder.com` if both are specified). */ export const getCookieDomain = (host: string, proxyDomains: string[]): string | undefined => { const idx = host.lastIndexOf(":") host = idx !== -1 ? host.substring(0, idx) : host + // If any of these are true we will still set cookies but without an explicit + // `Domain` attribute on the cookie. if ( - // Might be blank/missing, so there's nothing more to do. + // The host can be be blank or missing so there's nothing we can set. !host || // IP addresses can't have subdomains so there's no value in setting the - // domain for them. Assume anything with a : is ipv6 (valid domain name - // characters are alphanumeric or dashes). + // domain for them. Assume that anything with a : is ipv6 (valid domain name + // characters are alphanumeric or dashes)... host.includes(":") || - // Assume anything entirely numbers and dots is ipv4 (currently tlds + // ...and that anything entirely numbers and dots is ipv4 (currently tlds // cannot be entirely numbers). !/[^0-9.]/.test(host) || - // localhost subdomains don't seem to work at all (browser bug?). + // localhost subdomains don't seem to work at all (browser bug?). A cookie + // set at dev.localhost cannot be read by 8080.dev.localhost. host.endsWith(".localhost") || - // It might be localhost (or an IP, see above) if it's a proxy and it - // isn't setting the host header to match the access domain. - host === "localhost" + // Domains without at least one dot (technically two since domain.tld will + // become .domain.tld) are considered invalid according to the spec so don't + // set the domain for them. In my testing though localhost is the only + // problem (the browser just doesn't store the cookie at all). localhost has + // an additional problem which is that a reverse proxy might give + // code-server localhost even though the domain is really domain.tld (by + // default NGINX does this). + !host.includes(".") ) { logger.debug("no valid cookie doman", field("host", host)) return undefined From 72931edcf010c2e6ce9872f7e1ab974f999e09cb Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 11:16:21 -0600 Subject: [PATCH 76/82] Fix cleanup after getting message from vscode --- src/node/vscode.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 9ba7b8a90..6761a6d18 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -123,8 +123,19 @@ export class VscodeProvider { fn: (message: ipc.VscodeMessage) => message is T, ): Promise { return new Promise((resolve, _reject) => { - const reject = (error: Error) => { + const cleanup = () => { + proc.off("error", reject) + proc.off("exit", onExit) clearTimeout(timeout) + } + + const timeout = setTimeout(() => { + cleanup() + _reject(new Error("timed out")) + }, this.timeoutInterval) + + const reject = (error: Error) => { + cleanup() _reject(error) } @@ -132,16 +143,10 @@ export class VscodeProvider { reject(new Error(`VS Code exited unexpectedly with code ${code}`)) } - const timeout = setTimeout(() => { - reject(new Error("timed out")) - }, this.timeoutInterval) - proc.on("message", (message: ipc.VscodeMessage) => { logger.debug("got message from vscode", field("message", message)) - proc.off("error", reject) - proc.off("exit", onExit) if (fn(message)) { - clearTimeout(timeout) + cleanup() resolve(message) } }) From 31b67062b013ec5039c96e534e52891462edc2b1 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 11:17:45 -0600 Subject: [PATCH 77/82] Remove from onMessage Turns out that while Typescript can't infer the callback return type from it, Typescript can do the opposite and infer it from the callback return type. --- src/node/vscode.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 6761a6d18..089a3d201 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -69,7 +69,7 @@ export class VscodeProvider { vscode, ) - const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { + const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => { // There can be parallel initializations so wait for the right ID. return message.type === "options" && message.id === id }) @@ -104,7 +104,7 @@ export class VscodeProvider { dispose() }) - this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { + this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => { return message.type === "ready" }).then(() => vscode) From 5499a3d12562f4b5b15cb684f53428a8f68798e6 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 11:23:52 -0600 Subject: [PATCH 78/82] Use baseUrl when redirecting from domain proxy This will make the route more robust since it'll work under more than just the root. --- src/node/routes/domainProxy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index fdffdc585..25745955e 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -1,5 +1,6 @@ import { Request, Router } from "express" import { HttpCode, HttpError } from "../../common/http" +import { normalize } from "../../common/util" import { authenticated, ensureAuthenticated, redirect } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -54,8 +55,9 @@ router.all("*", (req, res, next) => { return next() } // Redirect all other pages to the login. + const to = normalize(`${req.baseUrl}${req.path}`) return redirect(req, res, "login", { - to: req.path !== "/" ? req.path : undefined, + to: to !== "/" ? to : undefined, }) } From b73ea2fea2bbc80f5f363d9f40d1687d2396b280 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:03:28 -0600 Subject: [PATCH 79/82] Unbind message handler itself after getting message Also switch `once` to `on` since we `off` them later anyway so no point in making Node do it twice. --- src/node/vscode.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 4d933e73e..9ab22ef78 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -126,6 +126,7 @@ export class VscodeProvider { const cleanup = () => { proc.off("error", reject) proc.off("exit", onExit) + proc.off("message", onMessage) clearTimeout(timeout) } @@ -143,16 +144,17 @@ export class VscodeProvider { reject(new Error(`VS Code exited unexpectedly with code ${code}`)) } - proc.on("message", (message: ipc.VscodeMessage) => { + const onMessage = (message: ipc.VscodeMessage) => { logger.trace("got message from vscode", field("message", message)) if (fn(message)) { cleanup() resolve(message) } - }) + } - proc.once("error", reject) - proc.once("exit", onExit) + proc.on("message", onMessage) + proc.on("error", reject) + proc.on("exit", onExit) }) } From 6f14b8b8dd123ae6b2b969dba3b920c5f81d3f06 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:07:45 -0600 Subject: [PATCH 80/82] Add separate handler for error Feels like it parallels better with the other handlers. --- src/node/vscode.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/node/vscode.ts b/src/node/vscode.ts index 9ab22ef78..c3da8a24d 100644 --- a/src/node/vscode.ts +++ b/src/node/vscode.ts @@ -122,9 +122,9 @@ export class VscodeProvider { proc: cp.ChildProcess, fn: (message: ipc.VscodeMessage) => message is T, ): Promise { - return new Promise((resolve, _reject) => { + return new Promise((resolve, reject) => { const cleanup = () => { - proc.off("error", reject) + proc.off("error", onError) proc.off("exit", onExit) proc.off("message", onMessage) clearTimeout(timeout) @@ -132,15 +132,16 @@ export class VscodeProvider { const timeout = setTimeout(() => { cleanup() - _reject(new Error("timed out")) + reject(new Error("timed out")) }, this.timeoutInterval) - const reject = (error: Error) => { + const onError = (error: Error) => { cleanup() - _reject(error) + reject(error) } const onExit = (code: number | null) => { + cleanup() reject(new Error(`VS Code exited unexpectedly with code ${code}`)) } @@ -153,7 +154,7 @@ export class VscodeProvider { } proc.on("message", onMessage) - proc.on("error", reject) + proc.on("error", onError) proc.on("exit", onExit) }) } From 96995b78d1e8eaeb20a0c7f535b60b48b07b35cf Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:29:41 -0600 Subject: [PATCH 81/82] Update cert flag test --- test/cli.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cli.test.ts b/test/cli.test.ts index a59aac995..6b1e96c2f 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -252,9 +252,9 @@ describe("parser", () => { _: [], ...defaults, cert: { - value: path.join(tmpdir, "self-signed.cert"), + value: path.join(paths.data, "localhost.crt"), }, - "cert-key": path.join(tmpdir, "self-signed.key"), + "cert-key": path.join(paths.data, "localhost.key"), }) }) From 9889f30224b8d68e9fbbbded709dbdb408e018ed Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 12 Nov 2020 12:30:41 -0600 Subject: [PATCH 82/82] Remove unused ts-expect-error from VS Code I'm not sure why other builds are passing with this still in. --- ci/dev/vscode.patch | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index ca67cb28c..9860ba965 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -210,6 +210,18 @@ index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073 } function processLib() { +diff --git a/extensions/typescript-language-features/src/utils/platform.ts b/extensions/typescript-language-features/src/utils/platform.ts +index 2d754bf4054713f53beed030f9211b33532c1b4b..708b7e40a662e4ca93420992bf7a5af0c62ea5b2 100644 +--- a/extensions/typescript-language-features/src/utils/platform.ts ++++ b/extensions/typescript-language-features/src/utils/platform.ts +@@ -6,6 +6,6 @@ + import * as vscode from 'vscode'; + + export function isWeb(): boolean { +- // @ts-expect-error ++ // NOTE@coder: Remove unused ts-expect-error directive which causes tsc to error. + return typeof navigator !== 'undefined' && vscode.env.uiKind === vscode.UIKind.Web; + } diff --git a/package.json b/package.json index 770b44b0c1ff53d903b7680ede27715376df00f2..b27ab71647a3e7c4b6076ba4fdb8fde20fa73bb0 100644 --- a/package.json