Merge pull request #2563 from cdr/proxy-path-passthrough-0bb9

pathProxy.ts: Implement --proxy-path-passthrough
This commit is contained in:
Anmol Sethi 2021-01-20 02:44:29 -05:00 committed by GitHub
commit 28e98c0ee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 277 additions and 95 deletions

View File

@ -15,6 +15,7 @@
- [How do I securely access web services?](#how-do-i-securely-access-web-services)
- [Sub-paths](#sub-paths)
- [Sub-domains](#sub-domains)
- [Why does the code-server proxy strip `/proxy/<port>` from the request path?](#why-does-the-code-server-proxy-strip-proxyport-from-the-request-path)
- [Multi-tenancy](#multi-tenancy)
- [Docker in code-server container?](#docker-in-code-server-container)
- [How can I disable telemetry?](#how-can-i-disable-telemetry)
@ -208,6 +209,42 @@ code-server --proxy-domain <domain>
Now you can browse to `<port>.<domain>`. Note that this uses the host header so
ensure your reverse proxy forwards that information if you are using one.
## Why does the code-server proxy strip `/proxy/<port>` from the request path?
HTTP servers should strive to use relative URLs to avoid needed to be coupled to the
absolute path at which they are served. This means you must use trailing slashes on all
paths with subpaths. See https://blog.cdivilly.com/2019/02/28/uri-trailing-slashes
This is really the "correct" way things work and why the striping of the base path is the
default. If your application uses relative URLs and does not assume the absolute path at
which it is being served, it will just work no matter what port you decide to serve it off
or if you put it in behind code-server or any other proxy!
However many people prefer the cleaner aesthetic of no trailing slashes. This couples you
to the base path as you cannot use relative redirects correctly anymore. See the above
link.
For users who are ok with this tradeoff, pass `--proxy-path-passthrough` to code-server
and the path will be passed as is.
This is particularly a problem with the `start` script in create-react-app. See
[#2222](https://github.com/cdr/code-server/issues/2222). You will need to inform
create-react-app of the path at which you are serving via `homepage` field in your
`package.json`. e.g. you'd add the following for the default CRA port:
```json
"homepage": "/proxy/3000",
```
Then visit `https://my-code-server-address.io/proxy/3000` to see your app exposed through
code-server!
Unfortunately `webpack-dev-server`'s websocket connections will not go through as it
always uses `/sockjs-node`. So hot reloading will not work until the `create-react-app`
team fix this bug.
Highly recommend using the subdomain approach instead to avoid this class of issue.
## Multi-tenancy
If you want to run multiple code-servers on shared infrastructure, we recommend using virtual

View File

@ -38,13 +38,13 @@
"@types/js-yaml": "^3.12.3",
"@types/mocha": "^8.0.3",
"@types/node": "^12.12.7",
"@types/node-fetch": "^2.5.7",
"@types/parcel-bundler": "^1.12.1",
"@types/pem": "^1.9.5",
"@types/proxy-from-env": "^1.0.1",
"@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",
@ -61,7 +61,6 @@
"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"
},
@ -81,6 +80,7 @@
"httpolyglot": "^0.1.2",
"js-yaml": "^3.13.1",
"limiter": "^1.1.5",
"node-fetch": "^2.6.1",
"pem": "^1.14.2",
"proxy-agent": "^4.0.0",
"proxy-from-env": "^1.1.0",

View File

@ -112,3 +112,11 @@ export const getFirstString = (value: string | string[] | object | undefined): s
return typeof value === "string" ? value : undefined
}
export function logError(prefix: string, err: any): void {
if (err instanceof Error) {
logger.error(`${prefix}: ${err.message} ${err.stack}`)
} else {
logger.error(`${prefix}: ${err}`)
}
}

View File

@ -3,6 +3,7 @@ import express, { Express } from "express"
import { promises as fs } from "fs"
import http from "http"
import * as httpolyglot from "httpolyglot"
import * as util from "../common/util"
import { DefaultedArgs } from "./cli"
import { handleUpgrade } from "./wsRouter"
@ -22,8 +23,21 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
)
: http.createServer(app)
await new Promise<http.Server>(async (resolve, reject) => {
server.on("error", reject)
let resolved = false
await new Promise<http.Server>(async (resolve2, reject) => {
const resolve = () => {
resolved = true
resolve2()
}
server.on("error", (err) => {
if (!resolved) {
reject(err)
} else {
// Promise resolved earlier so this is an unrelated error.
util.logError("http server error", err)
}
})
if (args.socket) {
try {
await fs.unlink(args.socket)

View File

@ -50,6 +50,7 @@ export interface Args extends VsArgs {
"show-versions"?: boolean
"uninstall-extension"?: string[]
"proxy-domain"?: string[]
"proxy-path-passthrough"?: boolean
locale?: string
_: string[]
"reuse-window"?: boolean
@ -172,6 +173,10 @@ const options: Options<Required<Args>> = {
"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." },
"proxy-path-passthrough": {
type: "boolean",
description: "Whether the path proxy should leave the /proxy/<port> in the request path when proxying.",
},
"ignore-last-opened": {
type: "boolean",
short: "e",
@ -239,7 +244,7 @@ export const optionDescriptions = (): string[] => {
export const parse = (
argv: string[],
opts?: {
configFile: string
configFile?: string
},
): Args => {
const error = (msg: string): Error => {
@ -516,7 +521,19 @@ export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
}
const configFile = await fs.readFile(configPath)
const config = yaml.safeLoad(configFile.toString(), {
return parseConfigFile(configFile.toString(), configPath)
}
/**
* parseConfigFile parses configFile into ConfigArgs.
* configPath is used as the filename in error messages
*/
export function parseConfigFile(configFile: string, configPath: string): ConfigArgs {
if (!configFile) {
return { _: [], config: configPath }
}
const config = yaml.safeLoad(configFile, {
filename: configPath,
})
if (!config || typeof config === "string") {

View File

@ -45,4 +45,13 @@ export class Heart {
})
}, this.heartbeatInterval)
}
/**
* Call to clear any heartbeatTimer for shutdown.
*/
public dispose(): void {
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
}
}

View File

@ -55,6 +55,9 @@ export const register = async (
})
})
})
server.on("close", () => {
heart.dispose()
})
app.disable("x-powered-by")
wsApp.disable("x-powered-by")
@ -165,7 +168,7 @@ export const register = async (
app.use(errorHandler)
const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => {
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
logger.error(`${err.message} ${err.stack}`)
;(req as WebsocketRequest).ws.end()
}

View File

@ -8,12 +8,12 @@ import { Router as WsRouter } from "../wsRouter"
export const router = Router()
const getProxyTarget = (req: Request, rewrite: boolean): string => {
if (rewrite) {
const query = qs.stringify(req.query)
return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}`
const getProxyTarget = (req: Request, passthroughPath: boolean): string => {
if (passthroughPath) {
return `http://0.0.0.0:${req.params.port}/${req.originalUrl}`
}
return `http://0.0.0.0:${req.params.port}/${req.originalUrl}`
const query = qs.stringify(req.query)
return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}`
}
router.all("/(:port)(/*)?", (req, res) => {
@ -33,7 +33,7 @@ router.all("/(:port)(/*)?", (req, res) => {
proxy.web(req, res, {
ignorePath: true,
target: getProxyTarget(req, true),
target: getProxyTarget(req, req.args["proxy-path-passthrough"] || false),
})
})
@ -42,6 +42,6 @@ export const wsRouter = WsRouter()
wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => {
proxy.ws(req, req.ws, req.head, {
ignorePath: true,
target: getProxyTarget(req, true),
target: getProxyTarget(req, req.args["proxy-path-passthrough"] || false),
})
})

72
test/httpserver.ts Normal file
View File

@ -0,0 +1,72 @@
import * as http from "http"
import * as nodeFetch from "node-fetch"
import * as util from "../src/common/util"
import { ensureAddress } from "../src/node/app"
// Perhaps an abstraction similar to this should be used in app.ts as well.
export class HttpServer {
private hs = http.createServer()
public constructor(hs?: http.Server) {
// See usage in test/integration.ts
if (hs) {
this.hs = hs
}
}
/**
* listen starts the server on a random localhost port.
* Use close to cleanup when done.
*/
public listen(fn: http.RequestListener): Promise<void> {
this.hs.on("request", fn)
let resolved = false
return new Promise((res, rej) => {
this.hs.listen(0, "localhost", () => {
res()
resolved = true
})
this.hs.on("error", (err) => {
if (!resolved) {
rej(err)
} else {
// Promise resolved earlier so this is some other error.
util.logError("http server error", err)
}
})
})
}
/**
* close cleans up the server.
*/
public close(): Promise<void> {
return new Promise((res, rej) => {
this.hs.close((err) => {
if (err) {
rej(err)
return
}
res()
})
})
}
/**
* fetch fetches the request path.
* The request path must be rooted!
*/
public fetch(requestPath: string, opts?: nodeFetch.RequestInit): Promise<nodeFetch.Response> {
return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts)
}
public port(): number {
const addr = this.hs.address()
if (addr && typeof addr === "object") {
return addr.port
}
throw new Error("server not listening or listening on unix socket")
}
}

21
test/integration.ts Normal file
View File

@ -0,0 +1,21 @@
import * as express from "express"
import { createApp } from "../src/node/app"
import { parse, setDefaults, parseConfigFile, DefaultedArgs } from "../src/node/cli"
import { register } from "../src/node/routes"
import * as httpserver from "./httpserver"
export async function setup(
argv: string[],
configFile?: string,
): Promise<[express.Application, express.Application, httpserver.HttpServer, DefaultedArgs]> {
argv = ["--bind-addr=localhost:0", ...argv]
const cliArgs = parse(argv)
const configArgs = parseConfigFile(configFile || "", "test/integration.ts")
const args = await setDefaults(cliArgs, configArgs)
const [app, wsApp, server] = await createApp(args)
await register(app, wsApp, server, args)
return [app, wsApp, new httpserver.HttpServer(server), args]
}

View File

@ -1,11 +1,12 @@
import { logger } from "@coder/logger"
import * as assert from "assert"
import * as express from "express"
import * as fs from "fs"
import { describe } from "mocha"
import * as path from "path"
import * as supertest from "supertest"
import { PluginAPI } from "../src/node/plugin"
import * as apps from "../src/node/routes/apps"
import * as httpserver from "./httpserver"
const fsp = fs.promises
/**
@ -13,23 +14,30 @@ const fsp = fs.promises
*/
describe("plugin", () => {
let papi: PluginAPI
let app: express.Application
let agent: supertest.SuperAgentTest
let s: httpserver.HttpServer
before(async () => {
papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow")
papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`)
await papi.loadPlugins()
app = express.default()
const app = express.default()
papi.mount(app)
app.use("/api/applications", apps.router(papi))
agent = supertest.agent(app)
s = new httpserver.HttpServer()
await s.listen(app)
})
after(async () => {
await s.close()
})
it("/api/applications", async () => {
await agent.get("/api/applications").expect(200, [
const resp = await s.fetch("/api/applications")
assert.equal(200, resp.status)
const body = await resp.json()
logger.debug(`${JSON.stringify(body)}`)
assert.deepEqual(body, [
{
name: "Test App",
version: "4.0.0",
@ -57,6 +65,9 @@ describe("plugin", () => {
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)
const resp = await s.fetch("/test-plugin/test-app")
assert.equal(200, resp.status)
const body = await resp.text()
assert.equal(body, indexHTML)
})
})

47
test/proxy.test.ts Normal file
View File

@ -0,0 +1,47 @@
import * as assert from "assert"
import * as express from "express"
import * as httpserver from "./httpserver"
import * as integration from "./integration"
describe("proxy", () => {
let codeServer: httpserver.HttpServer | undefined
const nhooyrDevServer = new httpserver.HttpServer()
let proxyPath: string
before(async () => {
const e = express.default()
await nhooyrDevServer.listen(e)
e.get("/wsup", (req, res) => {
res.json("asher is the best")
})
proxyPath = `/proxy/${nhooyrDevServer.port()}/wsup`
e.get(proxyPath, (req, res) => {
res.json("joe is the best")
})
})
after(async () => {
await nhooyrDevServer.close()
})
afterEach(async () => {
if (codeServer) {
await codeServer.close()
codeServer = undefined
}
})
it("should rewrite the base path", async () => {
;[, , codeServer] = await integration.setup(["--auth=none"], "")
const resp = await codeServer.fetch(proxyPath)
assert.equal(resp.status, 200)
assert.equal(await resp.json(), "asher is the best")
})
it("should not rewrite the base path", async () => {
;[, , codeServer] = await integration.setup(["--auth=none", "--proxy-path-passthrough=true"], "")
const resp = await codeServer.fetch(proxyPath)
assert.equal(resp.status, 200)
assert.equal(await resp.json(), "joe is the best")
})
})

View File

@ -1040,11 +1040,6 @@
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/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"
@ -1108,6 +1103,14 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.0.3.tgz#51b21b6acb6d1b923bbdc7725c38f9f455166402"
integrity sha512-vyxR57nv8NfcU0GZu8EUXZLTbCMupIUwy95LJ6lllN+JRPG25CwMHoB1q5xKh8YKhQnHYRAn4yW2yuHbf/5xgg==
"@types/node-fetch@^2.5.7":
version "2.5.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*", "@types/node@^12.12.7":
version "12.12.67"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.67.tgz#4f86badb292e822e3b13730a1f9713ed2377f789"
@ -1184,21 +1187,6 @@
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"
@ -2255,7 +2243,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.3.0:
component-emitter@^1.2.1:
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==
@ -2327,11 +2315,6 @@ 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"
@ -3428,11 +3411,6 @@ 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"
@ -3603,11 +3581,6 @@ 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"
@ -4978,7 +4951,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=
@ -5035,11 +5008,6 @@ 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"
@ -5214,6 +5182,11 @@ node-addon-api@^1.7.1:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-forge@^0.7.1:
version "0.7.6"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
@ -6405,11 +6378,6 @@ 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"
@ -7522,31 +7490,6 @@ 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"