From 74da5167a2a379a218f68c1f53cfe472d8699731 Mon Sep 17 00:00:00 2001 From: Ryan Brainard <966764+ryanbrainard@users.noreply.github.com> Date: Fri, 21 Jul 2023 19:23:21 -0400 Subject: [PATCH] Add --disable-proxy option (#6349) --- docs/FAQ.md | 14 ++++++++++++++ src/node/cli.ts | 9 +++++++++ src/node/http.ts | 19 +++++++++++++++++++ src/node/routes/domainProxy.ts | 6 +++++- src/node/routes/pathProxy.ts | 5 ++++- test/unit/node/cli.test.ts | 28 ++++++++++++++++++++++++++++ test/unit/node/proxy.test.ts | 11 +++++++++++ 7 files changed, 90 insertions(+), 2 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 71c0e1fd6..730450fe5 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -34,6 +34,7 @@ - [Are there community projects involving code-server?](#are-there-community-projects-involving-code-server) - [How do I change the port?](#how-do-i-change-the-port) - [How do I hide the coder/coder promotion in Help: Getting Started?](#how-do-i-hide-the-codercoder-promotion-in-help-getting-started) +- [How do I disable the proxy?](#how-do-i-disable-the-proxy) - [How do I disable file download?](#how-do-i-disable-file-download) @@ -453,6 +454,19 @@ You can pass the flag `--disable-getting-started-override` to `code-server` or you can set the environment variable `CS_DISABLE_GETTING_STARTED_OVERRIDE=1` or `CS_DISABLE_GETTING_STARTED_OVERRIDE=true`. +## How do I disable the proxy? + +You can pass the flag `--disable-proxy` to `code-server` or +you can set the environment variable `CS_DISABLE_PROXY=1` or +`CS_DISABLE_PROXY=true`. + +Note, this option currently only disables the proxy routes to forwarded ports, including +the domain and path proxy routes over HTTP and WebSocket; however, it does not +disable the automatic port forwarding in the VS Code workbench itself. In other words, +user will still see the Ports tab and notifications, but will not be able to actually +use access the ports. It is recommended to set `remote.autoForwardPorts` to `false` +when using the option. + ## How do I disable file download? You can pass the flag `--disable-file-downloads` to `code-server` diff --git a/src/node/cli.ts b/src/node/cli.ts index 3f3c8086d..8978490c1 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -51,6 +51,7 @@ export interface UserProvidedCodeArgs { "disable-file-downloads"?: boolean "disable-workspace-trust"?: boolean "disable-getting-started-override"?: boolean + "disable-proxy"?: boolean "session-socket"?: string } @@ -178,6 +179,10 @@ export const options: Options> = { type: "boolean", description: "Disable the coder/coder override in the Help: Getting Started page.", }, + "disable-proxy": { + type: "boolean", + description: "Disable domain and path proxy routes.", + }, // --enable can be used to enable experimental features. These features // provide no guarantees. enable: { type: "string[]" }, @@ -564,6 +569,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args["disable-getting-started-override"] = true } + if (process.env.CS_DISABLE_PROXY?.match(/^(1|true)$/)) { + args["disable-proxy"] = true + } + const usingEnvHashedPassword = !!process.env.HASHED_PASSWORD if (process.env.HASHED_PASSWORD) { args["hashed-password"] = process.env.HASHED_PASSWORD diff --git a/src/node/http.ts b/src/node/http.ts index 4158f0b81..837e12acf 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -75,6 +75,25 @@ export const replaceTemplates = ( .replace("{{OPTIONS}}", () => escapeJSON(serverOptions)) } +/** + * Throw an error if proxy is not enabled. Call `next` if provided. + */ +export const ensureProxyEnabled = (req: express.Request, _?: express.Response, next?: express.NextFunction): void => { + if (!proxyEnabled(req)) { + throw new HttpError("Forbidden", HttpCode.Forbidden) + } + if (next) { + next() + } +} + +/** + * Return true if proxy is enabled. + */ +export const proxyEnabled = (req: express.Request): boolean => { + return !req.args["disable-proxy"] +} + /** * Throw an error if not authorized. Call `next` if provided. */ diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 177d5a297..8f0537acc 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -1,6 +1,6 @@ import { Request, Router } from "express" import { HttpCode, HttpError } from "../../common/http" -import { getHost, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" +import { getHost, ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy } from "../proxy" import { Router as WsRouter } from "../wsRouter" @@ -59,6 +59,8 @@ router.all("*", async (req, res, next) => { return next() } + ensureProxyEnabled(req) + // Must be authenticated to use the proxy. const isAuthenticated = await authenticated(req) if (!isAuthenticated) { @@ -100,6 +102,8 @@ wsRouter.ws("*", async (req, _, next) => { if (!port) { return next() } + + ensureProxyEnabled(req) ensureOrigin(req) await ensureAuthenticated(req) proxy.ws(req, req.ws, req.head, { diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index 6d3c067e1..e86a430f9 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -3,7 +3,7 @@ import * as path from "path" import * as qs from "qs" import * as pluginapi from "../../../typings/pluginapi" import { HttpCode, HttpError } from "../../common/http" -import { authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" +import { ensureProxyEnabled, authenticated, ensureAuthenticated, ensureOrigin, redirect, self } from "../http" import { proxy as _proxy } from "../proxy" const getProxyTarget = (req: Request, passthroughPath?: boolean): string => { @@ -21,6 +21,8 @@ export async function proxy( passthroughPath?: boolean }, ): Promise { + ensureProxyEnabled(req) + if (!(await authenticated(req))) { // If visiting the root (/:port only) redirect to the login page. if (!req.params[0] || req.params[0] === "/") { @@ -50,6 +52,7 @@ export async function wsProxy( passthroughPath?: boolean }, ): Promise { + ensureProxyEnabled(req) ensureOrigin(req) await ensureAuthenticated(req) _proxy.ws(req, req.ws, req.head, { diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index e5491aca6..ce463333a 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -47,6 +47,7 @@ describe("parser", () => { delete process.env.CS_DISABLE_FILE_DOWNLOADS delete process.env.CS_DISABLE_GETTING_STARTED_OVERRIDE delete process.env.VSCODE_PROXY_URI + delete process.env.CS_DISABLE_PROXY console.log = jest.fn() }) @@ -103,6 +104,8 @@ describe("parser", () => { "--disable-getting-started-override", + "--disable-proxy", + ["--session-socket", "/tmp/override-code-server-ipc-socket"], ["--host", "0.0.0.0"], @@ -123,6 +126,7 @@ describe("parser", () => { }, "disable-file-downloads": true, "disable-getting-started-override": true, + "disable-proxy": true, enable: ["feature1", "feature2"], help: true, host: "0.0.0.0", @@ -392,6 +396,30 @@ describe("parser", () => { }) }) + it("should use env var CS_DISABLE_PROXY", async () => { + process.env.CS_DISABLE_PROXY = "1" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-proxy": true, + }) + }) + + it("should use env var CS_DISABLE_PROXY set to true", async () => { + process.env.CS_DISABLE_PROXY = "true" + const args = parse([]) + expect(args).toEqual({}) + + const defaultArgs = await setDefaults(args) + expect(defaultArgs).toEqual({ + ...defaults, + "disable-proxy": true, + }) + }) + it("should error if password passed in", () => { expect(() => parse(["--password", "supersecret123"])).toThrowError( "--password can only be set in the config file or passed in via $PASSWORD", diff --git a/test/unit/node/proxy.test.ts b/test/unit/node/proxy.test.ts index ea124e4f2..178f30665 100644 --- a/test/unit/node/proxy.test.ts +++ b/test/unit/node/proxy.test.ts @@ -45,6 +45,17 @@ describe("proxy", () => { 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")