The WebSocket origin check compared the browser's Origin host, which carries the port, against configured domains, which do not. publicOrigin only ever matches domains[0], so every other domain fell through to that comparison and was denied purely on the port: web.localhost:3000 never matched web.localhost. The result was a 403 on the HMR upgrade and a client reconnecting forever, while the page itself loaded fine because HTTP routing resolves the Host separately. Compares hostnames now. Unrelated and lookalike-suffix origins are still denied, and both cases are covered by tests. Verified through a real gateway: the HMR socket opens on both localhost and web.localhost, and a live edit reaches the browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { gatewayWebSocketOriginAllowed } from "../src/gateway.ts";
|
|
|
|
const target = {
|
|
name: "web",
|
|
origin: "http://127.0.0.1:3101",
|
|
domains: ["localhost", "web.localhost"],
|
|
publicOrigin: "http://localhost:3000",
|
|
} as any;
|
|
|
|
function upgrade(origin: string, host: string): Request {
|
|
return new Request("http://" + host + "/__wrnexus/hmr", {
|
|
headers: { origin, host, upgrade: "websocket" },
|
|
});
|
|
}
|
|
|
|
test("allows an upgrade from the app's primary domain", () => {
|
|
expect(
|
|
gatewayWebSocketOriginAllowed(upgrade("http://localhost:3000", "localhost:3000"), target, []),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("allows an upgrade from a secondary domain on a non-default port", () => {
|
|
// publicOrigin is built from domains[0], so a browser on web.localhost falls
|
|
// through to the domain list — where the origin host still carries :3000 and
|
|
// the configured domain does not. That mismatch denied every HMR socket on
|
|
// any domain but the first, leaving the client reconnecting forever.
|
|
expect(
|
|
gatewayWebSocketOriginAllowed(
|
|
upgrade("http://web.localhost:3000", "web.localhost:3000"),
|
|
target,
|
|
[],
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("still denies an unrelated origin", () => {
|
|
expect(
|
|
gatewayWebSocketOriginAllowed(
|
|
upgrade("http://evil.example:3000", "web.localhost:3000"),
|
|
target,
|
|
[],
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("still denies a lookalike suffix domain", () => {
|
|
expect(
|
|
gatewayWebSocketOriginAllowed(
|
|
upgrade("http://notweb.localhost:3000", "web.localhost:3000"),
|
|
target,
|
|
[],
|
|
),
|
|
).toBe(false);
|
|
});
|