diff --git a/CHANGELOG.md b/CHANGELOG.md index f7232093..207ca1cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [6.1.3](https://github.com/socketio/engine.io/compare/6.1.2...6.1.3) (2022-02-23) + + +### Bug Fixes + +* **typings:** allow CorsOptionsDelegate as cors options ([#641](https://github.com/socketio/engine.io/issues/641)) ([a463d26](https://github.com/socketio/engine.io/commit/a463d268ed90064e7863679bda423951de108c36)) +* **uws:** properly handle chunked content ([#642](https://github.com/socketio/engine.io/issues/642)) ([3367440](https://github.com/socketio/engine.io/commit/33674403084c329dc6ad026c4122333a6f8a9992)) + + + ## [6.1.2](https://github.com/socketio/engine.io/compare/6.1.1...6.1.2) (2022-01-18) diff --git a/lib/server.ts b/lib/server.ts index 44ce2ee6..739e38aa 100644 --- a/lib/server.ts +++ b/lib/server.ts @@ -9,7 +9,7 @@ import { serialize } from "cookie"; import { Server as DEFAULT_WS_ENGINE } from "ws"; import { IncomingMessage, Server as HttpServer } from "http"; import { CookieSerializeOptions } from "cookie"; -import { CorsOptions } from "cors"; +import { CorsOptions, CorsOptionsDelegate } from "cors"; const debug = debugModule("engine"); @@ -105,7 +105,7 @@ export interface ServerOptions { /** * the options that will be forwarded to the cors module */ - cors?: CorsOptions; + cors?: CorsOptions | CorsOptionsDelegate; /** * whether to enable compatibility with Socket.IO v2 clients * @default false diff --git a/lib/transports-uws/polling.ts b/lib/transports-uws/polling.ts index 8d6b9827..67581abd 100644 --- a/lib/transports-uws/polling.ts +++ b/lib/transports-uws/polling.ts @@ -122,6 +122,20 @@ export class Polling extends Transport { return; } + const expectedContentLength = Number(req.headers["content-length"]); + + if (!expectedContentLength) { + this.onError("content-length header required"); + res.writeStatus("411 Length Required").end(); + return; + } + + if (expectedContentLength > this.maxHttpBufferSize) { + this.onError("payload too large"); + res.writeStatus("413 Payload Too Large").end(); + return; + } + const isBinary = "application/octet-stream" === req.headers["content-type"]; if (isBinary && this.protocol === 4) { @@ -131,17 +145,8 @@ export class Polling extends Transport { this.dataReq = req; this.dataRes = res; - let chunks = []; - let contentLength = 0; - - const cleanup = () => { - this.dataReq = this.dataRes = chunks = null; - }; - - const onClose = () => { - cleanup(); - this.onError("data request connection closed prematurely"); - }; + let buffer; + let offset = 0; const headers = { // text/html is required instead of text/plain to avoid an @@ -150,36 +155,63 @@ export class Polling extends Transport { }; this.headers(req, headers); - Object.keys(headers).forEach(key => { + for (let key in headers) { res.writeHeader(key, String(headers[key])); - }); - - const onEnd = () => { - this.onData(Buffer.concat(chunks).toString()); + } - if (this.readyState !== "closing") { - res.end("ok"); - } - cleanup(); + const onEnd = buffer => { + this.onData(buffer.toString()); + this.onDataRequestCleanup(); + res.end("ok"); }; - res.onAborted(onClose); + res.onAborted(() => { + this.onDataRequestCleanup(); + this.onError("data request connection closed prematurely"); + }); - res.onData((chunk, isLast) => { - chunks.push(Buffer.from(chunk)); - contentLength += Buffer.byteLength(chunk); - if (contentLength > this.maxHttpBufferSize) { - this.onError("payload too large"); - res.writeStatus("413 Payload Too Large"); - res.end(); + res.onData((arrayBuffer, isLast) => { + const totalLength = offset + arrayBuffer.byteLength; + if (totalLength > expectedContentLength) { + this.onError("content-length mismatch"); + res.close(); // calls onAborted return; } + + if (!buffer) { + if (isLast) { + onEnd(Buffer.from(arrayBuffer)); + return; + } + buffer = Buffer.allocUnsafe(expectedContentLength); + } + + Buffer.from(arrayBuffer).copy(buffer, offset); + if (isLast) { - onEnd(); + if (totalLength != expectedContentLength) { + this.onError("content-length mismatch"); + res.writeStatus("400 Content-Length Mismatch").end(); + this.onDataRequestCleanup(); + return; + } + onEnd(buffer); + return; } + + offset = totalLength; }); } + /** + * Cleanup request. + * + * @api private + */ + private onDataRequestCleanup() { + this.dataReq = this.dataRes = null; + } + /** * Processes the incoming data payload. * diff --git a/package-lock.json b/package-lock.json index c93faa7b..9e8dca05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "engine.io", - "version": "6.1.2", + "version": "6.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "engine.io", - "version": "6.1.1", + "version": "6.1.3", "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", diff --git a/package.json b/package.json index cac0c586..4ec39013 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "engine.io", - "version": "6.1.2", + "version": "6.1.3", "description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server", "type": "commonjs", "main": "./build/engine.io.js", @@ -39,7 +39,7 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.0", + "engine.io-parser": "~5.0.3", "ws": "~8.2.3" }, "devDependencies": { diff --git a/test/server.js b/test/server.js index d7d64428..fff27d58 100644 --- a/test/server.js +++ b/test/server.js @@ -1955,6 +1955,65 @@ describe("server", () => { }); }); + it("should arrive when content is split in multiple chunks (polling)", done => { + const engine = listen( + { + maxHttpBufferSize: 1e10 + }, + port => { + const client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"] + }); + + engine.on("connection", socket => { + socket.on("message", data => { + client.close(); + done(); + }); + }); + + client.on("open", () => { + client.send("a".repeat(1e6)); + }); + } + ); + }); + + it("should arrive when content is sent with chunked transfer-encoding (polling)", function(done) { + if (process.env.EIO_WS_ENGINE === "uws") { + // µWebSockets.js does not currently support chunked encoding: https://github.com/uNetworking/uWebSockets.js/issues/669 + return this.skip(); + } + const engine = listen(port => { + const client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"] + }); + + engine.on("connection", socket => { + socket.on("message", data => { + expect(data).to.eql("123"); + + client.close(); + done(); + }); + }); + + client.on("open", () => { + const req = http.request({ + host: "localhost", + port, + path: `/engine.io/?EIO=4&transport=polling&sid=${client.id}`, + method: "POST" + }); + + req.write(process.env.EIO_CLIENT === "3" ? "4:41" : "41"); + req.write("2"); + req.write("3"); + req.end(); + }); + }); + }); + it("should arrive as ArrayBuffer if requested when binary data sent as Buffer (polling)", done => { const binaryData = Buffer.allocUnsafe(5); for (let i = 0; i < binaryData.length; i++) {