diff --git a/src/bun.js/bindings/webcore/JSWebSocket.cpp b/src/bun.js/bindings/webcore/JSWebSocket.cpp index b26cc913d3..e45d46a027 100644 --- a/src/bun.js/bindings/webcore/JSWebSocket.cpp +++ b/src/bun.js/bindings/webcore/JSWebSocket.cpp @@ -267,6 +267,16 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG RETURN_IF_EXCEPTION(throwScope, {}); } + // Check for rejectUnauthorized at top level for Node.js ws library compatibility + // Only check if it wasn't already set from the tls object + if (rejectUnauthorized == -1) { + auto rejectUnauthorizedValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "rejectUnauthorized"_s))); + RETURN_IF_EXCEPTION(throwScope, {}); + if (rejectUnauthorizedValue && !rejectUnauthorizedValue.isUndefinedOrNull() && rejectUnauthorizedValue.isBoolean()) { + rejectUnauthorized = rejectUnauthorizedValue.asBoolean() ? 1 : 0; + } + } + // Parse proxy option - can be string or { url, headers } auto proxyValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "proxy"_s))); RETURN_IF_EXCEPTION(throwScope, {}); diff --git a/test/regression/issue/22870.test.ts b/test/regression/issue/22870.test.ts new file mode 100644 index 0000000000..2a0bfbe9ea --- /dev/null +++ b/test/regression/issue/22870.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from "bun:test"; +import { tls } from "harness"; + +// Test for https://github.com/oven-sh/bun/issues/22870 +// rejectUnauthorized should work at the top level of WebSocket options +// for compatibility with Node.js ws library +describe("WebSocket rejectUnauthorized option", () => { + it("should accept rejectUnauthorized at top level", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // This should work with rejectUnauthorized at top level + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + rejectUnauthorized: false, + }); + + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = e => reject(new Error(`WebSocket error: ${e}`)); + }); + + const closed = new Promise(resolve => { + ws.onclose = resolve; + }); + ws.close(); + await closed; + }); + + it("should still accept rejectUnauthorized nested in tls object", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // The nested tls object should still work + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + tls: { rejectUnauthorized: false }, + }); + + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = e => reject(new Error(`WebSocket error: ${e}`)); + }); + + const closed = new Promise(resolve => { + ws.onclose = resolve; + }); + ws.close(); + await closed; + }); + + it("should prefer tls.rejectUnauthorized over top-level rejectUnauthorized", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // When both are specified, tls.rejectUnauthorized should take precedence + // Here tls.rejectUnauthorized: false should allow connection even though top-level says true + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + rejectUnauthorized: true, + tls: { rejectUnauthorized: false }, + }); + + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = e => reject(new Error(`WebSocket error: ${e}`)); + }); + + const closed = new Promise(resolve => { + ws.onclose = resolve; + }); + ws.close(); + await closed; + }); + + it("should fail with rejectUnauthorized: true against self-signed cert", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // With rejectUnauthorized: true (or default), self-signed cert should be rejected + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + rejectUnauthorized: true, + }); + + const errored = await new Promise(resolve => { + ws.onopen = () => { + ws.close(); + resolve(false); + }; + ws.onerror = () => { + ws.close(); + resolve(true); + }; + }); + + expect(errored).toBe(true); + }); +});