import * as express from "express" import { HttpCode } from "../../../src/common/http" import { wss, Router as WsRouter } from "../../../src/node/wsRouter" import { mockLogger } from "../../utils/helpers" import * as httpserver from "../../utils/httpserver" import * as integration from "../../utils/integration" describe("proxy", () => { const proxyTarget = new httpserver.HttpServer() const wsApp = express.default() const wsRouter = WsRouter() let codeServer: httpserver.HttpServer | undefined let proxyPath: string let absProxyPath: string let e: express.Express beforeAll(async () => { wsApp.use("/", wsRouter.router) await proxyTarget.listen((req, res) => { e(req, res) }) proxyTarget.listenUpgrade(wsApp) proxyPath = `/proxy/${proxyTarget.port()}/wsup` absProxyPath = proxyPath.replace("/proxy/", "/absproxy/") }) afterAll(async () => { await proxyTarget.dispose() }) beforeEach(() => { e = express.default() mockLogger() delete process.env.PASSWORD }) afterEach(async () => { if (codeServer) { await codeServer.dispose() codeServer = undefined } jest.clearAllMocks() }) it("should return 403 Forbidden if proxy is disabled", async () => { e.get("/wsup", (req, res) => { res.json("you cannot see this") }) codeServer = await integration.setup(["--auth=none", "--disable-proxy"], "") const resp = await codeServer.fetch(proxyPath) expect(resp.status).toBe(403) const json = await resp.json() expect(json).toEqual({ error: "Forbidden" }) }) it("should rewrite the base path", async () => { e.get("/wsup", (req, res) => { res.json("asher is the best") }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath) expect(resp.status).toBe(200) const json = await resp.json() expect(json).toBe("asher is the best") }) it("should not rewrite the base path", async () => { e.get(absProxyPath, (req, res) => { res.json("joe is the best") }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(absProxyPath) expect(resp.status).toBe(200) const json = await resp.json() expect(json).toBe("joe is the best") }) it("should rewrite redirects", async () => { e.post("/wsup", (req, res) => { res.redirect(307, "/finale") }) e.post("/finale", (req, res) => { res.json("redirect success") }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath, { method: "POST", }) expect(resp.status).toBe(200) expect(await resp.json()).toBe("redirect success") }) it("should not rewrite redirects", async () => { const finalePath = absProxyPath.replace("/wsup", "/finale") e.post(absProxyPath, (req, res) => { res.redirect(307, finalePath) }) e.post(finalePath, (req, res) => { res.json("redirect success") }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(absProxyPath, { method: "POST", }) expect(resp.status).toBe(200) expect(await resp.json()).toBe("redirect success") }) it("should allow post bodies", async () => { e.use(express.json({ strict: false })) e.post("/wsup", (req, res) => { res.json(req.body) }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath, { method: "post", body: JSON.stringify("coder is the best"), headers: { "Content-Type": "application/json", }, }) expect(resp.status).toBe(200) expect(await resp.json()).toBe("coder is the best") }) it("should handle bad requests", async () => { e.use(express.json({ strict: false })) e.post("/wsup", (req, res) => { res.json(req.body) }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath, { method: "post", body: "coder is the best", headers: { "Content-Type": "application/json", }, }) expect(resp.status).toBe(400) expect(resp.statusText).toMatch("Bad Request") }) it("should handle invalid routes", async () => { e.post("/wsup", (req, res) => { res.json(req.body) }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(`${proxyPath}/hello`) expect(resp.status).toBe(404) expect(resp.statusText).toMatch("Not Found") }) it("should handle errors", async () => { e.use(express.json({ strict: false })) e.post("/wsup", (req, res) => { throw new Error("BROKEN") }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath, { method: "post", body: JSON.stringify("coder is the best"), headers: { "Content-Type": "application/json", }, }) expect(resp.status).toBe(500) expect(resp.statusText).toMatch("Internal Server Error") }) it("should pass origin check", async () => { wsRouter.ws("/wsup", async (req) => { wss.handleUpgrade(req, req.ws, req.head, (ws) => { ws.send("hello") req.ws.resume() }) }) codeServer = await integration.setup(["--auth=none"], "") const ws = await codeServer.wsWait(proxyPath, { headers: { host: "localhost:8080", origin: "https://localhost:8080", }, }) ws.terminate() }) it("should fail origin check", async () => { await expect(async () => { codeServer = await integration.setup(["--auth=none"], "") await codeServer.wsWait(proxyPath, { headers: { host: "localhost:8080", origin: "https://evil.org", }, }) }).rejects.toThrow() }) it("should proxy non-ASCII", async () => { e.get(/.*/, (req, res) => { res.json("ほげ") }) codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(proxyPath.replace("wsup", "ほげ")) expect(resp.status).toBe(200) const json = await resp.json() expect(json).toBe("ほげ") }) it("should not double-encode query variables", async () => { const spy = jest.fn() e.get(/.*/, (req, res) => { spy([req.originalUrl, req.query]) res.end() }) codeServer = await integration.setup(["--auth=none"], "") for (const test of [ { endpoint: proxyPath, query: { foo: "bar with spaces" }, expected: "/wsup?foo=bar+with+spaces", }, { endpoint: absProxyPath, query: { foo: "bar with spaces" }, expected: absProxyPath + "?foo=bar+with+spaces", }, { endpoint: proxyPath, query: { foo: "with-&-ampersand" }, expected: "/wsup?foo=with-%26-ampersand", }, { endpoint: absProxyPath, query: { foo: "with-&-ampersand" }, expected: absProxyPath + "?foo=with-%26-ampersand", }, { endpoint: absProxyPath, query: { foo: "ほげ ほげ" }, expected: absProxyPath + "?foo=%E3%81%BB%E3%81%92+%E3%81%BB%E3%81%92", }, { endpoint: proxyPath, query: { foo: "ほげ ほげ" }, expected: "/wsup?foo=%E3%81%BB%E3%81%92+%E3%81%BB%E3%81%92", }, ]) { spy.mockClear() const resp = await codeServer.fetch(test.endpoint, undefined, test.query) expect(resp.status).toBe(200) await resp.text() expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledWith([test.expected, test.query]) } }) it("should allow specifying an absproxy path", async () => { const prefixedPath = `/codeserver/app1${absProxyPath}` e.get(prefixedPath, (req, res) => { res.send("app being served behind a prefixed path") }) codeServer = await integration.setup(["--auth=none", "--abs-proxy-base-path=/codeserver/app1"], "") const resp = await codeServer.fetch(absProxyPath) expect(resp.status).toBe(200) const text = await resp.text() expect(text).toBe("app being served behind a prefixed path") }) it("should not allow OPTIONS without authentication by default", async () => { process.env.PASSWORD = "test" codeServer = await integration.setup(["--auth=password"]) const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) expect(resp.status).toBe(401) }) it("should allow OPTIONS with `skip-auth-preflight` flag", async () => { process.env.PASSWORD = "test" codeServer = await integration.setup(["--auth=password", "--skip-auth-preflight"]) e.post("/wsup", (req, res) => {}) const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) expect(resp.status).toBe(200) }) it("should return a 500 when no target is running ", async () => { const target = new httpserver.HttpServer() await target.listen(() => {}) const port = target.port() target.dispose() codeServer = await integration.setup(["--auth=none"], "") const resp = await codeServer.fetch(`/proxy/${port}/wsup`) expect(resp.status).toBe(HttpCode.ServerError) expect(resp.statusText).toBe("Internal Server Error") }) it("should strip token cookie", async () => { const token = "my-super-secure-token" process.env.HASHED_PASSWORD = token codeServer = await integration.setup(["--auth=password"]) // Set up a listener that just prints the cookies it got. e.get("/wsup/cookies", (req, res) => { res.writeHead(HttpCode.Ok, { "Content-Type": "text/plain" }) res.end(req.headers.cookie) }) // Send the token along with other cookies which should be preserved. // Encode one to make sure they are being re-encoded properly. const value = "hello=there" const encodedValue = encodeURIComponent(value) const resp = await codeServer.fetch(proxyPath + "/cookies", { headers: { cookie: `cookie1=${encodedValue}; code-server-session=${token}; cookie2=hello;`, }, }) // The proxied listener should not have printed the code-server token. expect(resp.status).toBe(200) const text = await resp.text() expect(text).toBe(`cookie1=${encodedValue}; cookie2=hello`) }) })