From 931f7c011a94b8caf9289a4f573866703224b55c Mon Sep 17 00:00:00 2001 From: helgehatt Date: Thu, 27 Mar 2025 08:58:03 +0100 Subject: [PATCH 1/3] Allow OPTIONS requests through domain proxy --- src/node/routes/domainProxy.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 0a9bb4a324f7..9b5011a16c74 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -69,6 +69,11 @@ router.all(/.*/, async (req, res, next) => { return next() } + // OPTIONS requests should be allowed without credentials + if (req.method === "OPTIONS") { + 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) From bccc4fa8688f937721251c30fb0da35262a18e92 Mon Sep 17 00:00:00 2001 From: helgehatt Date: Sat, 12 Apr 2025 13:12:27 +0200 Subject: [PATCH 2/3] Added skip-auth-preflight flag --- src/node/cli.ts | 2 ++ src/node/main.ts | 3 +++ src/node/routes/domainProxy.ts | 10 +++++----- src/node/routes/pathProxy.ts | 4 +++- test/unit/node/cli.test.ts | 3 +++ test/unit/node/proxy.test.ts | 15 +++++++++++++++ 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 9eb6e5163e8a..8f1a08ee2593 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -84,6 +84,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { "trusted-origins"?: string[] version?: boolean "proxy-domain"?: string[] + "skip-auth-preflight"?: boolean "reuse-window"?: boolean "new-window"?: boolean "ignore-last-opened"?: boolean @@ -252,6 +253,7 @@ export const options: Options> = { description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).", }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, + "skip-auth-preflight": { type: 'boolean', description: "Allows preflight requests through proxy without authentication." }, "ignore-last-opened": { type: "boolean", short: "e", diff --git a/src/node/main.ts b/src/node/main.ts index 990a7af792b1..04e4470b9088 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -163,6 +163,9 @@ export const runCodeServer = async ( logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`) args["proxy-domain"].forEach((domain) => logger.info(` - ${domain}`)) } + if (args["skip-auth-preflight"]) { + logger.info(" - Skipping authentication for preflight requests") + } if (process.env.VSCODE_PROXY_URI) { logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`) } diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 9b5011a16c74..75e3df1f2bf5 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -61,6 +61,11 @@ router.all(/.*/, async (req, res, next) => { ensureProxyEnabled(req) + if (req.method === "OPTIONS" && req.args['skip-auth-preflight']) { + // Allow preflight requests with `skip-auth-preflight` flag + return next() + } + // Must be authenticated to use the proxy. const isAuthenticated = await authenticated(req) if (!isAuthenticated) { @@ -69,11 +74,6 @@ router.all(/.*/, async (req, res, next) => { return next() } - // OPTIONS requests should be allowed without credentials - if (req.method === "OPTIONS") { - 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) diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index bb8efd40d832..d3d4cf6f7401 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -26,7 +26,9 @@ export async function proxy( ): Promise { ensureProxyEnabled(req) - if (!(await authenticated(req))) { + if (req.method === "OPTIONS" && req.args['skip-auth-preflight']) { + // Allow preflight requests with `skip-auth-preflight` flag + } else if (!(await authenticated(req))) { // If visiting the root (/:port only) redirect to the login page. if (!req.params.path || req.params.path === "/") { const to = self(req) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index e596549da100..552576fac4c9 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -108,6 +108,8 @@ describe("parser", () => { ["--abs-proxy-base-path", "/codeserver/app1"], + "--skip-auth-preflight", + ["--session-socket", "/tmp/override-code-server-ipc-socket"], ["--host", "0.0.0.0"], @@ -146,6 +148,7 @@ describe("parser", () => { "bind-addr": "192.169.0.1:8080", "session-socket": "/tmp/override-code-server-ipc-socket", "abs-proxy-base-path": "/codeserver/app1", + "skip-auth-preflight": true, }) }) diff --git a/test/unit/node/proxy.test.ts b/test/unit/node/proxy.test.ts index 186cd475b3e2..299d723a5250 100644 --- a/test/unit/node/proxy.test.ts +++ b/test/unit/node/proxy.test.ts @@ -268,6 +268,21 @@ describe("proxy", () => { 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) + }) }) // NOTE@jsjoeio From 88ff214e619226424dbcd8c8cd7fc27c74b751d1 Mon Sep 17 00:00:00 2001 From: helgehatt Date: Sat, 12 Apr 2025 13:13:49 +0200 Subject: [PATCH 3/3] Run prettier --- src/node/cli.ts | 5 ++++- src/node/routes/domainProxy.ts | 2 +- src/node/routes/pathProxy.ts | 2 +- test/unit/node/proxy.test.ts | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index 8f1a08ee2593..a07a18b0a260 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -253,7 +253,10 @@ export const options: Options> = { description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).", }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, - "skip-auth-preflight": { type: 'boolean', description: "Allows preflight requests through proxy without authentication." }, + "skip-auth-preflight": { + type: "boolean", + description: "Allows preflight requests through proxy without authentication.", + }, "ignore-last-opened": { type: "boolean", short: "e", diff --git a/src/node/routes/domainProxy.ts b/src/node/routes/domainProxy.ts index 75e3df1f2bf5..6ffee67fa002 100644 --- a/src/node/routes/domainProxy.ts +++ b/src/node/routes/domainProxy.ts @@ -61,7 +61,7 @@ router.all(/.*/, async (req, res, next) => { ensureProxyEnabled(req) - if (req.method === "OPTIONS" && req.args['skip-auth-preflight']) { + if (req.method === "OPTIONS" && req.args["skip-auth-preflight"]) { // Allow preflight requests with `skip-auth-preflight` flag return next() } diff --git a/src/node/routes/pathProxy.ts b/src/node/routes/pathProxy.ts index d3d4cf6f7401..254c5e623a79 100644 --- a/src/node/routes/pathProxy.ts +++ b/src/node/routes/pathProxy.ts @@ -26,7 +26,7 @@ export async function proxy( ): Promise { ensureProxyEnabled(req) - if (req.method === "OPTIONS" && req.args['skip-auth-preflight']) { + if (req.method === "OPTIONS" && req.args["skip-auth-preflight"]) { // Allow preflight requests with `skip-auth-preflight` flag } else if (!(await authenticated(req))) { // If visiting the root (/:port only) redirect to the login page. diff --git a/test/unit/node/proxy.test.ts b/test/unit/node/proxy.test.ts index 299d723a5250..b3509ed640df 100644 --- a/test/unit/node/proxy.test.ts +++ b/test/unit/node/proxy.test.ts @@ -272,7 +272,7 @@ describe("proxy", () => { 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"}) + const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) expect(resp.status).toBe(401) }) @@ -280,7 +280,7 @@ describe("proxy", () => { 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"}) + const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) expect(resp.status).toBe(200) }) })