Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/engine.io/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,28 @@ to a single process.
- `net.Stream`: TCP socket for the request
- `Buffer`: legacy tail bytes
- **Returns** `Server` for chaining
- **Attaching custom upgrade response headers**

When you call `Server#handleUpgrade` from your own `'upgrade'` listener
(see example (C) above), you can attach extra headers to the upgrade
response by setting `req[Symbol.for("engine.io:responseHeaders")]` to an
object before calling `handleUpgrade`. Each property is sent as an
HTTP header. Example for setting CORS headers without using the `cors`
option (which is incompatible with WebTransport):

```js
const httpServer = require("http").createServer();

httpServer.on("upgrade", (req, socket, head) => {
if (req.url?.startsWith("/engine.io/")) {
req[Symbol.for("engine.io:responseHeaders")] = {
"Access-Control-Allow-Origin": req.headers.origin || "*",
"Access-Control-Allow-Credentials": "true",
};
engine.handleUpgrade(req, socket, head);
}
});
```
- `attach`
- Attach this Server instance to an `http.Server`
- Captures `upgrade` requests for a `http.Server`. In other words, makes
Expand Down
12 changes: 9 additions & 3 deletions packages/engine.io/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import { objectFromEntries } from "./utils/objectFromEntries";

const debug = debugModule("engine");

const kResponseHeaders = Symbol("responseHeaders");
// Exposed via the runtime-shared `Symbol.for` registry so user code calling
// `Server#handleUpgrade` directly can attach additional upgrade-response
// headers (e.g. CORS headers when the `cors` middleware option is not used).
// See https://github.com/socketio/socket.io/issues/5497.
export const kResponseHeaders = Symbol.for("engine.io:responseHeaders");

type TransportName = "polling" | "websocket" | "webtransport";

Expand Down Expand Up @@ -675,8 +679,10 @@ class WebSocketResponse {
readonly req,
readonly socket: Duplex,
) {
// temporarily store the response headers on the req object (see the "headers" event)
req[kResponseHeaders] = {};
// temporarily store the response headers on the req object (see the "headers" event).
// preserve any additional headers that the caller attached to the request before
// calling Server#handleUpgrade (see issue #5497).
req[kResponseHeaders] = { ...(req[kResponseHeaders] || {}) };
}

public setHeader(name: string, value: any) {
Expand Down
51 changes: 51 additions & 0 deletions packages/engine.io/test/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3684,6 +3684,57 @@ describe("server", () => {
});
});
});

it("should forward extra upgrade-response headers set via the shared-symbol hook", function (done) {
if (process.env.EIO_WS_ENGINE === "eiows") {
return this.skip();
}
// kResponseHeaders used to be a private Symbol unique to this module,
// so user code calling Server#handleUpgrade directly could not attach
// additional upgrade-response headers. See issue #5497.
const kResponseHeaders = Symbol.for("engine.io:responseHeaders");

const httpServer = http.createServer();
const localEngine = new Server();
let extraSeen = false;

// Capture the engine's emitted 'headers' payload once the upgrade is
// through. We only need to confirm the extra headers arrive; the
// handshake itself is validated by the connected client below.
localEngine.on("headers", (headers) => {
if (
headers["x-test-cors"] === "*" &&
headers["x-test-extra"] === "ok"
) {
extraSeen = true;
}
});

httpServer.listen(0, () => {
const port = httpServer.address().port;

httpServer.on("upgrade", (req, socket, head) => {
if (req.url?.startsWith("/engine.io/")) {
req[kResponseHeaders] = {
"x-test-cors": "*",
"x-test-extra": "ok",
};
return localEngine.handleUpgrade(req, socket, head);
}
});

client = new ClientSocket(`ws://localhost:${port}`);

client.on("open", () => {
client.close();
httpServer.close(() => {
expect(extraSeen).to.be(true);
done();
});
});
});
engine = localEngine;
});
});

describe("cors", () => {
Expand Down