diff --git a/doc/FAQ.md b/doc/FAQ.md index b9051e1a..0aff1886 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -65,6 +65,35 @@ only to HTTP requests. You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate for free. +## How do I securely access web services? + +code-server is capable of proxying to any port using either a subdomain or a +subpath which means you can securely access these services using code-server's +built-in authentication. + +### Sub-domains + +You will need a DNS entry that points to your server for each port you want to +access. You can either set up a wildcard DNS entry for `*.` if your domain +name registrar supports it or you can create one for every port you want to +access (`3000.`, `8080.`, etc). + +You should also set up TLS certificates for these subdomains, either using a +wildcard certificate for `*.` or individual certificates for each port. + +Start code-server with the `--proxy-domain` flag set to your domain. + +``` +code-server --proxy-domain +``` + +Now you can browse to `.`. Note that this uses the host header so +ensure your reverse proxy forwards that information if you are using one. + +### Sub-paths + +Just browse to `/proxy//`. + ## x86 releases? node has dropped support for x86 and so we decided to as well. See diff --git a/package.json b/package.json index 7856054e..842aed16 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@types/adm-zip": "^0.4.32", "@types/fs-extra": "^8.0.1", + "@types/http-proxy": "^1.17.4", "@types/mocha": "^5.2.7", "@types/node": "^12.12.7", "@types/parcel-bundler": "^1.12.1", @@ -52,13 +53,14 @@ "@coder/logger": "1.1.11", "adm-zip": "^0.4.14", "fs-extra": "^8.1.0", + "http-proxy": "^1.18.0", "httpolyglot": "^0.1.2", "node-pty": "^0.9.0", "pem": "^1.14.2", "safe-compare": "^1.1.4", "semver": "^7.1.3", - "tar": "^6.0.1", "ssh2": "^0.8.7", + "tar": "^6.0.1", "tar-fs": "^2.0.0", "ws": "^7.2.0" } diff --git a/src/browser/pages/home.html b/src/browser/pages/home.html index 08643f48..542f3b6e 100644 --- a/src/browser/pages/home.html +++ b/src/browser/pages/home.html @@ -17,7 +17,7 @@ href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials" /> - + diff --git a/src/node/app/api.ts b/src/node/app/api.ts index 78375fb6..88519ee3 100644 --- a/src/node/app/api.ts +++ b/src/node/app/api.ts @@ -43,7 +43,7 @@ export class ApiHttpProvider extends HttpProvider { public async handleRequest(route: Route, request: http.IncomingMessage): Promise { this.ensureAuthenticated(request) - if (route.requestPath !== "/index.html") { + if (!this.isRoot(route)) { throw new HttpError("Not found", HttpCode.NotFound) } diff --git a/src/node/app/dashboard.ts b/src/node/app/dashboard.ts index 21721495..261e93c5 100644 --- a/src/node/app/dashboard.ts +++ b/src/node/app/dashboard.ts @@ -20,7 +20,7 @@ export class DashboardHttpProvider extends HttpProvider { } public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (route.requestPath !== "/index.html") { + if (!this.isRoot(route)) { throw new HttpError("Not found", HttpCode.NotFound) } diff --git a/src/node/app/login.ts b/src/node/app/login.ts index 598b13ab..b55f5503 100644 --- a/src/node/app/login.ts +++ b/src/node/app/login.ts @@ -18,7 +18,7 @@ interface LoginPayload { */ export class LoginHttpProvider extends HttpProvider { public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") { + if (this.options.auth !== AuthType.Password || !this.isRoot(route)) { throw new HttpError("Not found", HttpCode.NotFound) } switch (route.base) { diff --git a/src/node/app/proxy.ts b/src/node/app/proxy.ts new file mode 100644 index 00000000..eff5059c --- /dev/null +++ b/src/node/app/proxy.ts @@ -0,0 +1,43 @@ +import * as http from "http" +import { HttpCode, HttpError } from "../../common/http" +import { HttpProvider, HttpResponse, Route, WsResponse } from "../http" + +/** + * 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) + } + + // 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: { + base: `${this.options.base}/${port}`, + port, + }, + } + } + + public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise { + this.ensureAuthenticated(request) + const port = route.base.replace(/^\//, "") + return { + proxy: { + base: `${this.options.base}/${port}`, + port, + }, + } + } +} diff --git a/src/node/app/update.ts b/src/node/app/update.ts index 7ee9f531..6acbc571 100644 --- a/src/node/app/update.ts +++ b/src/node/app/update.ts @@ -61,7 +61,7 @@ export class UpdateHttpProvider extends HttpProvider { this.ensureAuthenticated(request) this.ensureMethod(request) - if (route.requestPath !== "/index.html") { + if (!this.isRoot(route)) { throw new HttpError("Not found", HttpCode.NotFound) } diff --git a/src/node/app/vscode.ts b/src/node/app/vscode.ts index 6c9d10bb..ece990dc 100644 --- a/src/node/app/vscode.ts +++ b/src/node/app/vscode.ts @@ -126,7 +126,7 @@ export class VscodeHttpProvider extends HttpProvider { switch (route.base) { case "/": - if (route.requestPath !== "/index.html") { + if (!this.isRoot(route)) { throw new HttpError("Not found", HttpCode.NotFound) } else if (!this.authenticated(request)) { return { redirect: "/login", query: { to: this.options.base } } diff --git a/src/node/cli.ts b/src/node/cli.ts index 23006147..8feaf982 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -39,6 +39,7 @@ export interface Args extends VsArgs { readonly "install-extension"?: string[] readonly "show-versions"?: boolean readonly "uninstall-extension"?: string[] + readonly "proxy-domain"?: string[] readonly locale?: string readonly _: string[] } @@ -111,6 +112,7 @@ const options: Options> = { "install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." }, "uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." }, "show-versions": { type: "boolean", description: "Show VS Code extension versions." }, + "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, locale: { type: "string" }, log: { type: LogLevel }, diff --git a/src/node/entry.ts b/src/node/entry.ts index a784338f..26a235cf 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -5,11 +5,12 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc" import { ApiHttpProvider } from "./app/api" import { DashboardHttpProvider } from "./app/dashboard" 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 { Args, optionDescriptions, parse } from "./cli" -import { AuthType, HttpServer } from "./http" +import { AuthType, HttpServer, HttpServerOptions } from "./http" import { SshProvider } from "./ssh/server" import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util" import { ipcMain, wrap } from "./wrapper" @@ -36,42 +37,31 @@ const main = async (args: Args): Promise => { const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword())) // Spawn the main HTTP server. - const options = { + const options: HttpServerOptions = { auth, - cert: args.cert ? args.cert.value : undefined, - certKey: args["cert-key"], - sshHostKey: args["ssh-host-key"], commit, host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"), password: originalPassword ? hash(originalPassword) : undefined, port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080, + proxyDomains: args["proxy-domain"], socket: args.socket, + ...(args.cert && !args.cert.value + ? await generateCertificate() + : { + cert: args.cert && args.cert.value, + certKey: args["cert-key"], + }), } - if (!options.cert && args.cert) { - const { cert, certKey } = await generateCertificate() - options.cert = cert - options.certKey = certKey - } else if (args.cert && !args["cert-key"]) { + if (options.cert && !options.certKey) { throw new Error("--cert-key is missing") } - if (!args["disable-ssh"]) { - if (!options.sshHostKey && typeof options.sshHostKey !== "undefined") { - throw new Error("--ssh-host-key cannot be blank") - } else if (!options.sshHostKey) { - try { - options.sshHostKey = await generateSshHostKey() - } catch (error) { - logger.error("Unable to start SSH server", field("error", error.message)) - } - } - } - const httpServer = new HttpServer(options) const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args) const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"]) const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"]) + httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/login", LoginHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update) @@ -84,7 +74,7 @@ const main = async (args: Args): Promise => { if (auth === AuthType.Password && !process.env.PASSWORD) { logger.info(` - Password is ${originalPassword}`) - logger.info(" - To use your own password, set the PASSWORD environment variable") + logger.info(" - To use your own password set the PASSWORD environment variable") if (!args.auth) { logger.info(" - To disable use `--auth none`") } @@ -96,7 +86,7 @@ const main = async (args: Args): Promise => { if (httpServer.protocol === "https") { logger.info( - typeof args.cert === "string" + args.cert && args.cert.value ? ` - Using provided certificate and key for HTTPS` : ` - Using generated certificate and key for HTTPS`, ) @@ -104,11 +94,25 @@ const main = async (args: Args): Promise => { logger.info(" - Not serving HTTPS") } + if (httpServer.proxyDomains.size > 0) { + logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`) + httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`)) + } + logger.info(`Automatic updates are ${update.enabled ? "enabled" : "disabled"}`) + let sshHostKey = args["ssh-host-key"] + if (!args["disable-ssh"] && !sshHostKey) { + try { + sshHostKey = await generateSshHostKey() + } catch (error) { + logger.error("Unable to start SSH server", field("error", error.message)) + } + } + let sshPort: number | undefined - if (!args["disable-ssh"] && options.sshHostKey) { - const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string) + if (!args["disable-ssh"] && sshHostKey) { + const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, sshHostKey) try { sshPort = await sshProvider.listen() } catch (error) { @@ -118,6 +122,7 @@ const main = async (args: Args): Promise => { if (typeof sshPort !== "undefined") { logger.info(`SSH server listening on localhost:${sshPort}`) + logger.info(" - To disable use `--disable-ssh`") } else { logger.info("SSH server disabled") } diff --git a/src/node/http.ts b/src/node/http.ts index 06b7a167..654a9d79 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -1,6 +1,7 @@ import { field, logger } from "@coder/logger" import * as fs from "fs-extra" 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" @@ -18,6 +19,10 @@ import { getMediaMime, xdgLocalDir } 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[] } @@ -29,6 +34,17 @@ export enum AuthType { export type Query = { [key: string]: string | string[] | undefined } +export interface ProxyOptions { + /** + * A base path to strip from from the request before proxying if necessary. + */ + base?: string + /** + * The port to proxy. + */ + port: string +} + export interface HttpResponse { /* * Whether to set cache-control headers for this response. @@ -77,6 +93,17 @@ export interface HttpResponse { * `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 } /** @@ -100,14 +127,31 @@ export interface HttpServerOptions { readonly host?: string readonly password?: string readonly port?: number + readonly proxyDomains?: string[] readonly socket?: string } export interface Route { + /** + * Base path part (in /test/path it would be "/test"). + */ base: string + /** + * Remaining part of the route (in /test/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 } @@ -136,7 +180,9 @@ export abstract class HttpProvider { } /** - * Handle web sockets on the registered endpoint. + * 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 */ @@ -145,7 +191,7 @@ export abstract class HttpProvider { _socket: net.Socket, _head: Buffer, /* eslint-enable @typescript-eslint/no-unused-vars */ - ): Promise { + ): Promise { throw new HttpError("Not found", HttpCode.NotFound) } @@ -264,7 +310,7 @@ export abstract class HttpProvider { * Return the provided password value if the payload contains the right * password otherwise return false. If no payload is specified use cookies. */ - protected authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean { + public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean { switch (this.options.auth) { case AuthType.None: return true @@ -335,6 +381,14 @@ export abstract class HttpProvider { } 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" + } } /** @@ -407,7 +461,18 @@ export class HttpServer { private 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(xdgLocalDir, "heartbeat"), async () => { const connections = await this.getConnections() logger.trace(`${connections} active connection${plural(connections)}`) @@ -425,6 +490,16 @@ export class HttpServer { } 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 + } + }) } public dispose(): void { @@ -515,6 +590,9 @@ export class HttpServer { this.heart.beat() const route = this.parseUrl(request) const write = (payload: HttpResponse): void => { + const host = request.headers.host || "" + const idx = host.indexOf(":") + const domain = idx !== -1 ? host.substring(0, idx) : host 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) } : {}), @@ -525,9 +603,12 @@ export class HttpServer { "Set-Cookie": [ `${payload.cookie.key}=${payload.cookie.value}`, `Path=${normalize(payload.cookie.path || "/", true)}`, + domain ? `Domain=${this.getCookieDomain(domain)}` : undefined, // "HttpOnly", - "SameSite=strict", - ].join(";"), + "SameSite=lax", + ] + .filter((l) => !!l) + .join(";"), } : {}), ...payload.headers, @@ -547,20 +628,27 @@ export class HttpServer { response.end() } } + try { - const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request)) - if (!payload) { - throw new HttpError("Not found", HttpCode.NotFound) + const payload = + this.maybeRedirect(request, route) || + (route.provider.authenticated(request) && this.maybeProxy(request)) || + (await route.provider.handleRequest(route, request)) + if (payload.proxy) { + this.doProxy(route, request, response, payload.proxy) + } else { + write(payload) } - write(payload) } catch (error) { let e = error if (error.code === "ENOENT" || error.code === "EISDIR") { e = new HttpError("Not found", HttpCode.NotFound) } - logger.debug("Request error", field("url", request.url)) - logger.debug(error.stack) const code = typeof e.code === "number" ? e.code : HttpCode.ServerError + logger.debug("Request error", field("url", request.url), field("code", code)) + if (code >= HttpCode.ServerError) { + logger.error(error.stack) + } const payload = await route.provider.getErrorRoot(route, code, code, e.message) write({ code, @@ -625,7 +713,14 @@ export class HttpServer { throw new HttpError("Not found", HttpCode.NotFound) } - await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head) + // 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(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}`) @@ -647,7 +742,6 @@ export class HttpServer { // Happens if it's a plain `domain.com`. base = "/" } - requestPath = requestPath || "/index.html" return { base, requestPath } } @@ -670,4 +764,106 @@ export class HttpServer { } return { base, 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.base + + const isHttp = response instanceof http.ServerResponse + const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath + 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 domain that should be used for setting a cookie. This will allow + * the user to authenticate only once. This will return the highest level + * domain (e.g. `coder.com` over `test.coder.com` if both are specified). + */ + private getCookieDomain(host: string): string { + let current: string | undefined + this.proxyDomains.forEach((domain) => { + if (host.endsWith(domain) && (!current || domain.length < current.length)) { + current = domain + } + }) + // Setting the domain to localhost doesn't seem to work for subdomains (for + // example dev.localhost). + return current && current !== "localhost" ? current : host + } + + /** + * 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. + */ + public maybeProxy(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.proxyDomains.has(proxyDomain)) { + return undefined + } + + return { + proxy: { + port, + }, + } + } } diff --git a/test/cli.test.ts b/test/cli.test.ts index 9de3900e..aab12684 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -117,6 +117,7 @@ describe("cli", () => { assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/) assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/) assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/) + assert.throws(() => parse(["--ssh-host-key"]), /--ssh-host-key requires a value/) }) it("should error if value is invalid", () => { @@ -160,4 +161,19 @@ describe("cli", () => { auth: "none", }) }) + + it("should support repeatable flags", () => { + assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), { + _: [], + "extensions-dir": path.join(xdgLocalDir, "extensions"), + "user-data-dir": xdgLocalDir, + "proxy-domain": ["*.coder.com"], + }) + assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), { + _: [], + "extensions-dir": path.join(xdgLocalDir, "extensions"), + "user-data-dir": xdgLocalDir, + "proxy-domain": ["*.coder.com", "test.com"], + }) + }) }) diff --git a/yarn.lock b/yarn.lock index e14037d7..07042359 100644 --- a/yarn.lock +++ b/yarn.lock @@ -871,6 +871,13 @@ dependencies: "@types/node" "*" +"@types/http-proxy@^1.17.4": + version "1.17.4" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" + integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== + dependencies: + "@types/node" "*" + "@types/json-schema@^7.0.3": version "7.0.4" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" @@ -2240,7 +2247,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.2.6: +debug@3.2.6, debug@^3.0.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2745,6 +2752,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eventemitter3@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" + integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + events@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" @@ -2980,6 +2992,13 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +follow-redirects@^1.0.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb" + integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ== + dependencies: + debug "^3.0.0" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3403,6 +3422,15 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-proxy@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" + integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -5894,6 +5922,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"