diff --git a/packages/engine.io/.eslintrc.json b/packages/engine.io/.eslintrc.json new file mode 100644 index 00000000..082c0cb5 --- /dev/null +++ b/packages/engine.io/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": "prettier", + "parser": "babel-eslint" +} diff --git a/packages/engine.io/.prettierignore b/packages/engine.io/.prettierignore new file mode 100644 index 00000000..935d7792 --- /dev/null +++ b/packages/engine.io/.prettierignore @@ -0,0 +1 @@ +lib/parser-v3/ diff --git a/packages/engine.io/CHANGELOG.md b/packages/engine.io/CHANGELOG.md new file mode 100644 index 00000000..649a6014 --- /dev/null +++ b/packages/engine.io/CHANGELOG.md @@ -0,0 +1,851 @@ +# History + +| Version | Release date | +|------------------------------------------------------------------------------------------------------|----------------| +| [6.6.0](#660-2024-06-21) | June 2024 | +| [6.5.5](#655-2024-06-18) (from the [6.5.x](https://github.com/socketio/engine.io/tree/6.5.x) branch) | June 2024 | +| [3.6.2](#362-2024-06-18) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) | June 2024 | +| [6.5.4](#654-2023-11-09) | November 2023 | +| [6.5.3](#653-2023-10-06) | October 2023 | +| [6.5.2](#652-2023-08-01) | August 2023 | +| [6.5.1](#651-2023-06-27) | June 2023 | +| [6.5.0](#650-2023-06-16) | June 2023 | +| [6.4.2](#642-2023-05-02) | May 2023 | +| [6.4.1](#641-2023-02-20) | February 2023 | +| [6.4.0](#640-2023-02-06) | February 2023 | +| [6.3.1](#631-2023-01-12) | January 2023 | +| [6.3.0](#630-2023-01-10) | January 2023 | +| [3.6.1](#361-2022-11-20) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) | November 2022 | +| [6.2.1](#621-2022-11-20) | November 2022 | +| [3.6.0](#360-2022-06-06) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) | June 2022 | +| [6.2.0](#620-2022-04-17) | April 2022 | +| [6.1.3](#613-2022-02-23) | February 2022 | +| [6.1.2](#612-2022-01-18) | January 2022 | +| [6.1.1](#611-2022-01-11) | January 2021 | +| [6.1.0](#610-2021-11-08) | November 2022 | +| [6.0.1](#601-2021-11-06) | November 2021 | +| [**6.0.0**](#600-2021-10-08) | October 2021 | +| [5.2.0](#520-2021-08-29) | August 2021 | +| [5.1.1](#511-2021-05-16) | May 2021 | +| [5.1.0](#510-2021-05-04) | May 2021 | +| [**5.0.0**](#500-2021-03-10) | March 2021 | +| [4.1.1](#411-2021-02-02) | February 2021 | +| [4.1.0](#410-2021-01-14) | January 2021 | +| [4.0.6](#406-2021-01-04) | January 2021 | +| [3.5.0](#350-2020-12-30) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) | December 2020 | +| [4.0.5](#405-2020-12-07) | December 2020 | +| [4.0.4](#404-2020-11-17) | November 2020 | +| [4.0.3](#403-2020-11-17) | November 2020 | +| [4.0.2](#402-2020-11-09) | November 2020 | +| [4.0.1](#401-2020-10-21) | October 2020 | +| [**4.0.0**](#400-2020-09-10) | September 2020 | +| [3.4.2](#342-2020-06-04) | June 2020 | +| [3.4.1](#341-2020-04-17) | April 2020 | + + +# Release notes + +## [6.6.0](https://github.com/socketio/engine.io/compare/6.5.4...6.6.0) (2024-06-21) + + +### Bug Fixes + +* fix `websocket` and `webtransport` send callbacks ([#699](https://github.com/socketio/engine.io/issues/699)) ([fc21c4a](https://github.com/socketio/engine.io/commit/fc21c4a05f9d50d7efd62aa7a937fadce385e919)) +* properly call the send callback during upgrade ([362bc78](https://github.com/socketio/engine.io/commit/362bc78191c607e6b7c7f2b2e7e7ddb2fe53101c)) +* **types:** make socket.request writable ([#697](https://github.com/socketio/engine.io/issues/697)) ([0efa04b](https://github.com/socketio/engine.io/commit/0efa04b5841816d18b0c6ebf7c5f592f8382978a)) + + +### Performance Improvements + +* do not reset the hearbeat timer on each packet ([5359bae](https://github.com/socketio/engine.io/commit/5359bae683e2a25742bd4989d0355a8fc10d294e)) +* **websocket:** use bound callbacks ([9a68c8c](https://github.com/socketio/engine.io/commit/9a68c8ce93cc1bc0bc1a30548558da49860f4acd)) + + +### Dependencies + +- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change) + + + +## [6.5.5](https://github.com/socketio/engine.io/compare/6.5.4...6.5.5) (2024-06-18) + +This release contains a bump of the `ws` dependency, which includes an important [security fix](https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c). + +Advisory: https://github.com/advisories/GHSA-3h5v-q93c-6h6q + +### Bug Fixes + +* **types:** make socket.request writable ([#697](https://github.com/socketio/engine.io/issues/697)) ([0efa04b](https://github.com/socketio/engine.io/commit/0efa04b5841816d18b0c6ebf7c5f592f8382978a)) + +### Dependencies + +- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) ([diff](https://github.com/websockets/ws/compare/8.11.0...8.17.1)) + + + +## [3.6.2](https://github.com/socketio/engine.io/compare/3.6.1...3.6.2) (2024-06-18) + +This release contains a bump of the `ws` dependency, which includes an important [security fix](https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c). + +Advisory: https://github.com/advisories/GHSA-3h5v-q93c-6h6q + +### Dependencies + +- [`ws@~7.5.10`](https://github.com/websockets/ws/releases/tag/7.5.10) ([diff](https://github.com/websockets/ws/compare/7.4.2...7.5.10)) + + + +## [6.5.4](https://github.com/socketio/engine.io/compare/6.5.3...6.5.4) (2023-11-09) + +This release contains some minor changes which should improve the memory usage of the server, notably [this](https://github.com/socketio/engine.io/commit/f27a6c35017e4eb37546949f754e09933102837a). + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.5.3](https://github.com/socketio/engine.io/compare/6.5.2...6.5.3) (2023-10-06) + + +### Bug Fixes + +* improve compatibility with node16 module resolution ([#689](https://github.com/socketio/engine.io/issues/689)) ([c6bf8c0](https://github.com/socketio/engine.io/commit/c6bf8c0f571aad7a5917f43860c8c3d74a9b429b)) +* **webtransport:** properly handle abruptly closed connections ([ff1c861](https://github.com/socketio/engine.io/commit/ff1c8615483bab25acc9cf04fb40339b0bd78812)) + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.5.2](https://github.com/socketio/engine.io/compare/6.5.1...6.5.2) (2023-08-01) + + +### Bug Fixes + +* **webtransport:** add proper framing ([a306db0](https://github.com/socketio/engine.io/commit/a306db09e8ddb367c7d62f45fec920f979580b7c)) + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.5.1](https://github.com/socketio/engine.io/compare/6.5.0...6.5.1) (2023-06-27) + + +### Bug Fixes + +* prevent crash when accessing TextDecoder ([#684](https://github.com/socketio/engine.io/issues/684)) ([6dd2bc4](https://github.com/socketio/engine.io/commit/6dd2bc4f68edd7575c3844ae8ceadde649be95b2)) + + +### Credits + +Huge thanks to [@iowaguy](https://github.com/iowaguy) for helping! + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.5.0](https://github.com/socketio/engine.io/compare/6.4.2...6.5.0) (2023-06-16) + + +### Bug Fixes + +* **uws:** discard any write to an aborted uWS response ([#682](https://github.com/socketio/engine.io/issues/682)) ([3144d27](https://github.com/socketio/engine.io/commit/3144d274584ae3b96cca4e609c66c56d534f1715)) + + +### Features + +#### Support for WebTransport + +The Engine.IO server can now use WebTransport as the underlying transport. + +WebTransport is a web API that uses the HTTP/3 protocol as a bidirectional transport. It's intended for two-way communications between a web client and an HTTP/3 server. + +References: + +- https://w3c.github.io/webtransport/ +- https://developer.mozilla.org/en-US/docs/Web/API/WebTransport +- https://developer.chrome.com/articles/webtransport/ + +Until WebTransport support lands [in Node.js](https://github.com/nodejs/node/issues/38478), you can use the `@fails-components/webtransport` package: + +```js +import { readFileSync } from "fs"; +import { createServer } from "https"; +import { Server } from "engine.io"; +import { Http3Server } from "@fails-components/webtransport"; + +// WARNING: the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements) +const cert = readFileSync("/path/to/my/cert.pem"); +const key = readFileSync("/path/to/my/key.pem"); + +const httpsServer = createServer({ + key, + cert +}); + +httpsServer.listen(3000); + +const engine = new Server({ + transports: ["polling", "websocket", "webtransport"] // WebTransport is not enabled by default +}); + +engine.attach(httpsServer); + +const h3Server = new Http3Server({ + port: 3000, + host: "0.0.0.0", + secret: "changeit", + cert, + privKey: key, +}); + +(async () => { + const stream = await h3Server.sessionStream("/engine.io/"); + const sessionReader = stream.getReader(); + + while (true) { + const { done, value } = await sessionReader.read(); + if (done) { + break; + } + engine.onWebTransportSession(value); + } +})(); + +h3Server.startServer(); +``` + +Added in [123b68c](https://github.com/socketio/engine.io/commit/123b68c04f9e971f59b526e0f967a488ee6b0116). + + +### Credits + +Huge thanks to [@OxleyS](https://github.com/OxleyS) for helping! + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.4.2](https://github.com/socketio/engine.io/compare/6.4.1...6.4.2) (2023-05-02) + +:warning: This release contains an important security fix :warning: + +A malicious client could send a specially crafted HTTP request, triggering an uncaught exception and killing the Node.js process: + +``` +TypeError: Cannot read properties of undefined (reading 'handlesUpgrades') + at Server.onWebSocket (build/server.js:515:67) +``` + +Please upgrade as soon as possible. + + +### Bug Fixes + +* include error handling for Express middlewares ([#674](https://github.com/socketio/engine.io/issues/674)) ([9395782](https://github.com/socketio/engine.io/commit/93957828be1252c83275b56f0c7c0bd145a0ceb9)) +* prevent crash when provided with an invalid query param ([fc480b4](https://github.com/socketio/engine.io/commit/fc480b4f305e16fe5972cf337d055e598372dc44)) +* **typings:** make clientsCount public ([#675](https://github.com/socketio/engine.io/issues/675)) ([bd6d471](https://github.com/socketio/engine.io/commit/bd6d4713b02ff646c581872cd9ffe753acff0d73)) +* **uws:** prevent crash when using with middlewares ([8b22162](https://github.com/socketio/engine.io/commit/8b2216290330b174c9e67be32765bec0c74769f9)) + + +### Credits + +Huge thanks to [@tyilo](https://github.com/tyilo) and [@cieldeville](https://github.com/cieldeville) for helping! + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.4.1](https://github.com/socketio/engine.io/compare/6.4.0...6.4.1) (2023-02-20) + +This release contains [6e78489](https://github.com/socketio/engine.io/commit/6e78489486f0d7570861fd6002a364d1ab87da4a), which exports the `BaseServer` class in order to restore the compatibility with the `nodenext` module resolution strategy of TypeScript. + +Reference: https://www.typescriptlang.org/tsconfig/#moduleResolution + +Related: https://github.com/socketio/socket.io/issues/4621 + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + +## [6.4.0](https://github.com/socketio/engine.io/compare/6.3.1...6.4.0) (2023-02-06) + + +### Features + +* add support for Express middlewares ([24786e7](https://github.com/socketio/engine.io/commit/24786e77c5403b1c4b5a2bc84e2af06f9187f74a)) + +This commit implements middlewares at the Engine.IO level, because Socket.IO middlewares are meant for namespace authorization and are not executed during a classic HTTP request/response cycle. + +A workaround was possible by using the allowRequest option and the "headers" event, but this feels way cleaner and works with upgrade requests too. + +Syntax: + +```js +engine.use((req, res, next) => { + // do something + + next(); +}); + +// with express-session +import session from "express-session"; + +engine.use(session({ + secret: "keyboard cat", + resave: false, + saveUninitialized: true, + cookie: { secure: true } +})); + +// with helmet +import helmet from "helmet"; + +engine.use(helmet()); +``` + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.3.1](https://github.com/socketio/engine.io/compare/6.3.0...6.3.1) (2023-01-12) + + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change) + + + +## [6.3.0](https://github.com/socketio/engine.io/compare/6.2.1...6.3.0) (2023-01-10) + + +### Bug Fixes + +* fix the ES module wrapper ([ed87609](https://github.com/socketio/engine.io/commit/ed87609bafca0e844e6b29ea1a895d95df6a544c)) +* wait for all packets to be sent before closing the WebSocket connection ([a65a047](https://github.com/socketio/engine.io/commit/a65a047526401bebaa113a8c70d03f5d963eaa54)) + + +### Features + +* add the "addTrailingSlash" option ([#655](https://github.com/socketio/engine.io/issues/655)) ([d0fd474](https://github.com/socketio/engine.io/commit/d0fd4746afa396297f07bb62e539b0c1c4018d7c)) + +The trailing slash which was added by default can now be disabled: + +```js +import { Server } from "engine.io"; + +const server = new Server(); + +server.attach(httpServer, { + addTrailingSlash: false +}); +``` + +In the example above, the clients can omit the trailing slash and use `/engine.io` instead of `/engine.io/`. + + +### Performance Improvements + +* add the wsPreEncodedFrame option ([5e34722](https://github.com/socketio/engine.io/commit/5e34722b0b6564d6207a56d69bc3b0a831e4dc46)) + +This will be used when broadcasting packets at the Socket.IO level. + +See also: https://github.com/socketio/socket.io-adapter/commit/5f7b47d40f9daabe4e3c321eda620bbadfe5ce96 + +### Dependencies + +- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) ([diff](https://github.com/websockets/ws/compare/8.2.3...8.11.0)) + + + +## [3.6.1](https://github.com/socketio/engine.io/compare/3.6.0...3.6.1) (2022-11-20) + +:warning: This release contains an important security fix :warning: + +A malicious client could send a specially crafted HTTP request, triggering an uncaught exception and killing the Node.js process: + +``` +Error: read ECONNRESET + at TCP.onStreamRead (internal/stream_base_commons.js:209:20) +Emitted 'error' event on Socket instance at: + at emitErrorNT (internal/streams/destroy.js:106:8) + at emitErrorCloseNT (internal/streams/destroy.js:74:3) + at processTicksAndRejections (internal/process/task_queues.js:80:21) { + errno: -104, + code: 'ECONNRESET', + syscall: 'read' +} +``` + +Please upgrade as soon as possible. + +### Bug Fixes + +* catch errors when destroying invalid upgrades ([83c4071](https://github.com/socketio/engine.io/commit/83c4071af871fc188298d7d591e95670bf9f9085)) + +### Dependencies + +- [`ws@~7.4.2`](https://github.com/websockets/ws/releases/tag/7.4.2) (no change) + + +## [6.2.1](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1) (2022-11-20) + +:warning: This release contains an important security fix :warning: + +A malicious client could send a specially crafted HTTP request, triggering an uncaught exception and killing the Node.js process: + +``` +Error: read ECONNRESET + at TCP.onStreamRead (internal/stream_base_commons.js:209:20) +Emitted 'error' event on Socket instance at: + at emitErrorNT (internal/streams/destroy.js:106:8) + at emitErrorCloseNT (internal/streams/destroy.js:74:3) + at processTicksAndRejections (internal/process/task_queues.js:80:21) { + errno: -104, + code: 'ECONNRESET', + syscall: 'read' +} +``` + +Please upgrade as soon as possible. + +### Bug Fixes + +* catch errors when destroying invalid upgrades ([#658](https://github.com/socketio/engine.io/issues/658)) ([425e833](https://github.com/socketio/engine.io/commit/425e833ab13373edf1dd5a0706f07100db14e3c6)) + +### Dependencies + +- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change) + + + +## [3.6.0](https://github.com/socketio/engine.io/compare/3.5.0...3.6.0) (2022-06-06) + + +### Bug Fixes + +* add extension in the package.json main entry ([#608](https://github.com/socketio/engine.io/issues/608)) ([3ad0567](https://github.com/socketio/engine.io/commit/3ad0567dbd57cfb7c2ff4e8b7488d80f37022b4a)) +* do not reset the ping timer after upgrade ([1f5d469](https://github.com/socketio/engine.io/commit/1f5d4699862afee1e410fcb0e1f5e751ebcd2f9f)), closes [/github.com/socketio/socket.io-client-swift/pull/1309#issuecomment-768475704](https://github.com//github.com/socketio/socket.io-client-swift/pull/1309/issues/issuecomment-768475704) + + +### Features + +* decrease the default value of maxHttpBufferSize ([58e274c](https://github.com/socketio/engine.io/commit/58e274c437e9cbcf69fd913c813aad8fbd253703)) + +This change reduces the default value from 100 mb to a more sane 1 mb. + +This helps protect the server against denial of service attacks by malicious clients sending huge amounts of data. + +See also: https://github.com/advisories/GHSA-j4f2-536g-r55m + +* increase the default value of pingTimeout ([f55a79a](https://github.com/socketio/engine.io/commit/f55a79a28a5fbc6c9edae876dd11308b89cc979e)) + + + +## [6.2.0](https://github.com/socketio/engine.io/compare/6.1.3...6.2.0) (2022-04-17) + + +### Features + +* add the "maxPayload" field in the handshake details ([088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38)) + +So that clients in HTTP long-polling can decide how many packets they have to send to stay under the maxHttpBufferSize +value. + +This is a backward compatible change which should not mandate a new major revision of the protocol (we stay in v4), as +we only add a field in the JSON-encoded handshake data: + +``` +0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000} +``` + + + +## [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) + + +### Bug Fixes + +* **uws:** expose additional uWebSockets.js options ([#634](https://github.com/socketio/engine.io/issues/634)) ([49bb7cf](https://github.com/socketio/engine.io/commit/49bb7cf66518d4b49baf883a16ee1fe1ed8aed28)) +* **uws:** fix HTTP long-polling with CORS ([45112a3](https://github.com/socketio/engine.io/commit/45112a30d1af4cc25b21a5d658a748583cb64ed4)) +* **uws:** handle invalid websocket upgrades ([8b4d6a8](https://github.com/socketio/engine.io/commit/8b4d6a8176db72f5c2420c5a45f0d97d33af049b)) + + + +## [6.1.1](https://github.com/socketio/engine.io/compare/6.1.0...6.1.1) (2022-01-11) + +:warning: This release contains an important security fix :warning: + +A malicious client could send a specially crafted HTTP request, triggering an uncaught exception and killing the Node.js process: + +> RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear +> at Receiver.getInfo (/.../node_modules/ws/lib/receiver.js:176:14) +> at Receiver.startLoop (/.../node_modules/ws/lib/receiver.js:136:22) +> at Receiver._write (/.../node_modules/ws/lib/receiver.js:83:10) +> at writeOrBuffer (internal/streams/writable.js:358:12) + +This bug was introduced by [this commit](https://github.com/socketio/engine.io/commit/f3c291fa613a9d50c924d74293035737fdace4f2), included in `engine.io@4.0.0`, so previous releases are not impacted. + +Thanks to Marcus Wejderot from Mevisio for the responsible disclosure. + +### Bug Fixes + +* properly handle invalid data sent by a malicious websocket client ([c0e194d](https://github.com/socketio/engine.io/commit/c0e194d44933bd83bf9a4b126fca68ba7bf5098c)) + + + +## [6.1.0](https://github.com/socketio/engine.io/compare/6.0.0...6.1.0) (2021-11-08) + + +### Bug Fixes + +* fix payload encoding for v3 clients ([ed50fc3](https://github.com/socketio/engine.io/commit/ed50fc346b9c58459bf4e6fe5c45e8d34faac8da)) + + +### Features + +* add an implementation based on uWebSockets.js ([271e2df](https://github.com/socketio/engine.io/commit/271e2df94d39bbd13c33cab98cdd5915f9d28536)) + + +### Performance Improvements + +* refresh ping timer ([#628](https://github.com/socketio/engine.io/issues/628)) ([37474c7](https://github.com/socketio/engine.io/commit/37474c7e67be7c5f25f9ca2d4ea99f3a256bd2de)) + + + +## [6.0.1](https://github.com/socketio/engine.io/compare/6.0.0...6.0.1) (2021-11-06) + + +### Bug Fixes + +* fix payload encoding for v3 clients ([3f42262](https://github.com/socketio/engine.io/commit/3f42262fd27a77a7383cdbb44ede7c6211a9782b)) + + + +## [6.0.0](https://github.com/socketio/engine.io/compare/5.2.0...6.0.0) (2021-10-08) + +The codebase was migrated to TypeScript ([c0d6eaa](https://github.com/socketio/engine.io/commit/c0d6eaa1ba1291946dc8425d5f533d5f721862dd)) + +An ES module wrapper was also added ([401f4b6](https://github.com/socketio/engine.io/commit/401f4b60693fb6702c942692ce42e5bb701d81d7)). + +Please note that the communication protocol was not updated, so a v5 client will be able to reach a v6 server (and vice-versa). + +Reference: https://github.com/socketio/engine.io-protocol + +### BREAKING CHANGES + +- the default export was removed, so the following code won't work anymore: + +```js +const eioServer = require("engine.io")(httpServer); +``` + +Please use this instead: + +```js +const { Server } = require("engine.io"); +const eioServer = new Server(httpServer); +``` + +### Dependencies + +`ws` version: `~8.2.3` (bumped from `~7.4.2`) + +## [5.2.0](https://github.com/socketio/engine.io/compare/5.1.1...5.2.0) (2021-08-29) + +No change on the server-side, this matches the client release. + + +## [5.1.1](https://github.com/socketio/engine.io/compare/5.1.0...5.1.1) (2021-05-16) + + +### Bug Fixes + +* properly close the websocket connection upon handshake error ([4360686](https://github.com/socketio/engine.io/commit/43606865e5299747cbb31f3ed9baf4567502a879)) + + +## [5.1.0](https://github.com/socketio/engine.io/compare/5.0.0...5.1.0) (2021-05-04) + + +### Features + +* add a "connection_error" event ([7096e98](https://github.com/socketio/engine.io/commit/7096e98a02295a62c8ea2aa56461d4875887092d)) +* add the "initial_headers" and "headers" events ([2527543](https://github.com/socketio/engine.io/commit/252754353a0e88eb036ebb3082e9d6a9a5f497db)) + + +### Performance Improvements + +* **websocket:** add a "wsPreEncoded" writing option ([7706b12](https://github.com/socketio/engine.io/commit/7706b123df914777d19c8179b45ab6932f82916c)) +* **websocket:** fix write back-pressure ([#618](https://github.com/socketio/engine.io/issues/618)) ([ad5306a](https://github.com/socketio/engine.io/commit/ad5306aeaedf06ac7a49f791e1b76e55c35a564e)) + + +## [5.0.0](https://github.com/socketio/engine.io/compare/4.1.1...5.0.0) (2021-03-10) + + +### Bug Fixes + +* set default protocol version to 3 ([#616](https://github.com/socketio/engine.io/issues/616)) ([868d891](https://github.com/socketio/engine.io/commit/868d89111de0ab5bd0e147ecaff7983afbf5d087)) + + +### Features + +* increase the default value of pingTimeout ([5a7fa13](https://github.com/socketio/engine.io/commit/5a7fa132c442bc1e7eefa1cf38168ee951575ded)) +* remove dynamic require() with wsEngine ([edb7343](https://github.com/socketio/engine.io/commit/edb734316f143bf0f1bbc344e966d18e2676b934)) + + +### BREAKING CHANGES + +* the syntax of the "wsEngine" option is updated + +Before: + +```js +const eioServer = require("engine.io")(httpServer, { + wsEngine: "eiows" +}); +``` + +After: + +```js +const eioServer = require("engine.io")(httpServer, { + wsEngine: require("eiows").Server +}); +``` + + +## [4.1.1](https://github.com/socketio/engine.io/compare/4.1.0...4.1.1) (2021-02-02) + + +### Bug Fixes + +* do not reset the ping timer after upgrade ([ff2b8ab](https://github.com/socketio/engine.io/commit/ff2b8aba48ebcb0de5626d3b76fddc94c398395f)), closes [/github.com/socketio/socket.io-client-swift/pull/1309#issuecomment-768475704](https://github.com//github.com/socketio/socket.io-client-swift/pull/1309/issues/issuecomment-768475704) + + +## [4.1.0](https://github.com/socketio/engine.io/compare/4.0.6...4.1.0) (2021-01-14) + + +### Features + +* add support for v3.x clients ([663d326](https://github.com/socketio/engine.io/commit/663d326d18de598318bd2120b2b70cd51adf8955)) + + +## [4.0.6](https://github.com/socketio/engine.io/compare/4.0.5...4.0.6) (2021-01-04) + + +### Bug Fixes + +* correctly pass the options when using the Server constructor ([#610](https://github.com/socketio/engine.io/issues/610)) ([cec2750](https://github.com/socketio/engine.io/commit/cec27502f5b55c8a2ff289db34019629bf6a97ca)) + + + +## [3.5.0](https://github.com/socketio/engine.io/compare/3.4.2...3.5.0) (2020-12-30) + + +### Features + +* add support for all cookie options ([19cc582](https://github.com/socketio/engine.io/commit/19cc58264a06dca47ed401fbaca32dcdb80a903b)), closes [/github.com/jshttp/cookie#options-1](https://github.com//github.com/jshttp/cookie/issues/options-1) +* disable perMessageDeflate by default ([5ad2736](https://github.com/socketio/engine.io/commit/5ad273601eb66c7b318542f87026837bf9dddd21)) + + + +## [4.0.5](https://github.com/socketio/engine.io/compare/4.0.4...4.0.5) (2020-12-07) + +No change on the server-side, this matches the client release. + +## [4.0.4](https://github.com/socketio/engine.io/compare/4.0.3...4.0.4) (2020-11-17) + +No change on the server-side, this matches the client release. + +## [4.0.3](https://github.com/socketio/engine.io/compare/4.0.2...4.0.3) (2020-11-17) + +No change on the server-side, this matches the client release. + +## [4.0.2](https://github.com/socketio/engine.io/compare/4.0.1...4.0.2) (2020-11-09) + + +### Bug Fixes + +* add extension in the package.json main entry ([#608](https://github.com/socketio/engine.io/issues/608)) ([17b8c2f](https://github.com/socketio/engine.io/commit/17b8c2f199e7a307b6d6294b8599abacb3ec56e7)) + + +## [4.0.1](https://github.com/socketio/engine.io/compare/4.0.0...4.0.1) (2020-10-21) + + +### Bug Fixes + +* do not overwrite CORS headers upon error ([fe093ba](https://github.com/socketio/engine.io/commit/fe093bae1adce99e01dfdd3ce7542957785098b5)) + + + +## [4.0.0](https://github.com/socketio/engine.io/compare/v4.0.0-alpha.1...4.0.0) (2020-09-10) + +More details about this release in the blog post: https://socket.io/blog/engine-io-4-release/ + +### Bug Fixes + +* ignore errors when forcefully closing the socket ([#601](https://github.com/socketio/engine.io/issues/601)) ([dcdbccb](https://github.com/socketio/engine.io/commit/dcdbccb3dd8a7b7db057d23925356034fcd35d48)) +* remove implicit require of uws ([82cdca2](https://github.com/socketio/engine.io/commit/82cdca23bab0ed69b61b60961900d456a3065e6a)) + + +### Features + +* disable perMessageDeflate by default ([078527a](https://github.com/socketio/engine.io/commit/078527a384b70dc46d99083fa218be5d45213e51)) + +#### Links + +- Diff: [v4.0.0-alpha.1...4.0.0](https://github.com/socketio/engine.io/compare/v4.0.0-alpha.1...4.0.0) +- Full diff: [3.4.0...4.0.0](https://github.com/socketio/engine.io/compare/3.4.0...4.0.0) +- Client release: [4.0.0](https://github.com/socketio/engine.io-client/releases/tag/4.0.0) +- ws version: [^7.1.2](https://github.com/websockets/ws/releases/tag/7.1.2) + + +## [3.4.2](https://github.com/socketio/engine.io/compare/3.4.1...3.4.2) (2020-06-04) + + +### Bug Fixes + +* remove explicit require of uws ([85e544a](https://github.com/socketio/engine.io/commit/85e544afd95a5890761a613263a5eba0c9a18a93)) + +#### Links + +- Diff: [3.4.1...3.4.2](https://github.com/socketio/engine.io/compare/3.4.1...3.4.2) +- Client release: - +- ws version: [^7.1.2](https://github.com/websockets/ws/releases/tag/7.1.2) + + + +## [3.4.1](https://github.com/socketio/engine.io/compare/3.4.0...3.4.1) (2020-04-17) + + +### Bug Fixes + +* ignore errors when forcefully closing the socket ([da851ec](https://github.com/socketio/engine.io/commit/da851ec4ec89d96df2ee5c711f328b5d795423e9)) +* use SameSite=Strict by default ([001ca62](https://github.com/socketio/engine.io/commit/001ca62cc4a8f511f3b2fbd9e4493ad274a6a0e5)) + +#### Links + +- Diff: [3.4.0...3.4.1](https://github.com/socketio/engine.io/compare/3.4.0...3.4.1) +- Client release: [3.4.1](https://github.com/socketio/engine.io-client/releases/tag/3.4.1) +- ws version: [^7.1.2](https://github.com/websockets/ws/releases/tag/7.1.2) + + + +## [4.0.0-alpha.1](https://github.com/socketio/engine.io/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) (2020-02-12) + +#### Links + +- Diff: [v4.0.0-alpha.0...v4.0.0-alpha.1](https://github.com/socketio/engine.io-client/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) +- Client release: [v4.0.0-alpha.1](https://github.com/socketio/engine.io-client/releases/tag/v4.0.0-alpha.1) +- ws version: [^7.1.2](https://github.com/websockets/ws/releases/tag/7.1.2) + + + +## [4.0.0-alpha.0](https://github.com/socketio/engine.io/compare/3.4.0...v4.0.0-alpha.0) (2020-02-12) + + +### Features + +* decrease the default value of maxHttpBufferSize ([734f9d1](https://github.com/socketio/engine.io/commit/734f9d1268840722c41219e69eb58318e0b2ac6b)) +* disable cookie by default and add sameSite attribute ([a374471](https://github.com/socketio/engine.io/commit/a374471d06e3681a769766a1d068898182f9305f)), closes [/github.com/jshttp/cookie#options-1](https://github.com//github.com/jshttp/cookie/issues/options-1) +* generateId method can now return a Promise ([f3c291f](https://github.com/socketio/engine.io/commit/f3c291fa613a9d50c924d74293035737fdace4f2)) +* reverse the ping-pong mechanism ([31ff875](https://github.com/socketio/engine.io/commit/31ff87593f231b86dc47ec5761936439ebd53c20)) +* use the cors module to handle cross-origin requests ([61b9492](https://github.com/socketio/engine.io/commit/61b949259ed966ef6fc8bfd61f14d1a2ef06d319)) + + +### BREAKING CHANGES + +* the handlePreflightRequest option is removed by the change. + +Before: + +``` +new Server({ + handlePreflightRequest: (req, res) => { + res.writeHead(200, { + "Access-Control-Allow-Origin": 'https://example.com', + "Access-Control-Allow-Methods": 'GET', + "Access-Control-Allow-Headers": 'Authorization', + "Access-Control-Allow-Credentials": true + }); + res.end(); + } +}) +``` + +After: + +``` +new Server({ + cors: { + origin: "https://example.com", + methods: ["GET"], + allowedHeaders: ["Authorization"], + credentials: true + } +}) +``` +* the syntax has changed from + +``` +new Server({ + cookieName: "test", + cookieHttpOnly: false, + cookiePath: "/custom" +}) +``` + +to + +``` +new Server({ + cookie: { + name: "test", + httpOnly: false, + path: "/custom" + } +}) +``` + +All other options (domain, maxAge, sameSite, ...) are now supported. + +* v3.x clients will not be able to connect anymore (they will send a ping packet and timeout while waiting for a pong packet). + +#### Links + +- Diff: [3.4.0...v4.0.0-alpha.0](https://github.com/socketio/engine.io-client/compare/3.4.0...v4.0.0-alpha.0) +- Client release: [v4.0.0-alpha.0](https://github.com/socketio/engine.io-client/releases/tag/v4.0.0-alpha.0) +- ws version: [^7.1.2](https://github.com/websockets/ws/releases/tag/7.1.2) + diff --git a/packages/engine.io/LICENSE b/packages/engine.io/LICENSE new file mode 100644 index 00000000..6494c3c7 --- /dev/null +++ b/packages/engine.io/LICENSE @@ -0,0 +1,19 @@ +(The MIT License) + +Copyright (c) 2014 Guillermo Rauch + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/packages/engine.io/README.md b/packages/engine.io/README.md new file mode 100644 index 00000000..f33f788f --- /dev/null +++ b/packages/engine.io/README.md @@ -0,0 +1,603 @@ + +# Engine.IO: the realtime engine + +[![Build Status](https://github.com/socketio/engine.io/workflows/CI/badge.svg?branch=master)](https://github.com/socketio/engine.io/actions) +[![NPM version](https://badge.fury.io/js/engine.io.svg)](http://badge.fury.io/js/engine.io) + +`Engine.IO` is the implementation of transport-based +cross-browser/cross-device bi-directional communication layer for +[Socket.IO](http://github.com/socketio/socket.io). + +## How to use + +### Server + +#### (A) Listening on a port + +```js +const engine = require('engine.io'); +const server = engine.listen(80); + +server.on('connection', socket => { + socket.send('utf 8 string'); + socket.send(Buffer.from([0, 1, 2, 3, 4, 5])); // binary data +}); +``` + +#### (B) Intercepting requests for a http.Server + +```js +const engine = require('engine.io'); +const http = require('http').createServer().listen(3000); +const server = engine.attach(http); + +server.on('connection', socket => { + socket.on('message', data => { }); + socket.on('close', () => { }); +}); +``` + +#### (C) Passing in requests + +```js +const engine = require('engine.io'); +const server = new engine.Server(); + +server.on('connection', socket => { + socket.send('hi'); +}); + +// … +httpServer.on('upgrade', (req, socket, head) => { + server.handleUpgrade(req, socket, head); +}); + +httpServer.on('request', (req, res) => { + server.handleRequest(req, res); +}); +``` + +### Client + +```html + + +``` + +For more information on the client refer to the +[engine-client](http://github.com/socketio/engine.io-client) repository. + +## What features does it have? + +- **Maximum reliability**. Connections are established even in the presence of: + - proxies and load balancers. + - personal firewall and antivirus software. + - for more information refer to **Goals** and **Architecture** sections +- **Minimal client size** aided by: + - lazy loading of flash transports. + - lack of redundant transports. +- **Scalable** + - load balancer friendly +- **Future proof** +- **100% Node.JS core style** + - No API sugar (left for higher level projects) + +## API + +### Server + +

+ +#### Top-level + +These are exposed by `require('engine.io')`: + +##### Events + +- `flush` + - Called when a socket buffer is being flushed. + - **Arguments** + - `Socket`: socket being flushed + - `Array`: write buffer +- `drain` + - Called when a socket buffer is drained + - **Arguments** + - `Socket`: socket being flushed + +##### Properties + +- `protocol` _(Number)_: protocol revision number +- `Server`: Server class constructor +- `Socket`: Socket class constructor +- `Transport` _(Function)_: transport constructor +- `transports` _(Object)_: map of available transports + +##### Methods + +- `()` + - Returns a new `Server` instance. If the first argument is an `http.Server` then the + new `Server` instance will be attached to it. Otherwise, the arguments are passed + directly to the `Server` constructor. + - **Parameters** + - `http.Server`: optional, server to attach to. + - `Object`: optional, options object (see `Server#constructor` api docs below) + + The following are identical ways to instantiate a server and then attach it. + +```js +const httpServer; // previously created with `http.createServer();` from node.js api. + +// create a server first, and then attach +const eioServer = require('engine.io').Server(); +eioServer.attach(httpServer); + +// or call the module as a function to get `Server` +const eioServer = require('engine.io')(); +eioServer.attach(httpServer); + +// immediately attach +const eioServer = require('engine.io')(httpServer); + +// with custom options +const eioServer = require('engine.io')(httpServer, { + maxHttpBufferSize: 1e3 +}); +``` + +- `listen` + - Creates an `http.Server` which listens on the given port and attaches WS + to it. It returns `501 Not Implemented` for regular http requests. + - **Parameters** + - `Number`: port to listen on. + - `Object`: optional, options object + - `Function`: callback for `listen`. + - **Options** + - All options from `Server.attach` method, documented below. + - **Additionally** See Server `constructor` below for options you can pass for creating the new Server + - **Returns** `Server` + +```js +const engine = require('engine.io'); +const server = engine.listen(3000, { + pingTimeout: 2000, + pingInterval: 10000 +}); + +server.on('connection', /* ... */); +``` + +- `attach` + - Captures `upgrade` requests for a `http.Server`. In other words, makes + a regular http.Server WebSocket-compatible. + - **Parameters** + - `http.Server`: server to attach to. + - `Object`: optional, options object + - **Options** + - All options from `Server.attach` method, documented below. + - **Additionally** See Server `constructor` below for options you can pass for creating the new Server + - **Returns** `Server` a new Server instance. + +```js +const engine = require('engine.io'); +const httpServer = require('http').createServer().listen(3000); +const server = engine.attach(httpServer, { + wsEngine: require('eiows').Server // requires having eiows as dependency +}); + +server.on('connection', /* ... */); +``` + +#### Server + +The main server/manager. _Inherits from EventEmitter_. + +##### Events + +- `connection` + - Fired when a new connection is established. + - **Arguments** + - `Socket`: a Socket object + +- `initial_headers` + - Fired on the first request of the connection, before writing the response headers + - **Arguments** + - `headers` (`Object`): a hash of headers + - `req` (`http.IncomingMessage`): the request + +- `headers` + - Fired on the all requests of the connection, before writing the response headers + - **Arguments** + - `headers` (`Object`): a hash of headers + - `req` (`http.IncomingMessage`): the request + +- `connection_error` + - Fired when an error occurs when establishing the connection. + - **Arguments** + - `error`: an object with following properties: + - `req` (`http.IncomingMessage`): the request that was dropped + - `code` (`Number`): one of `Server.errors` + - `message` (`string`): one of `Server.errorMessages` + - `context` (`Object`): extra info about the error + +| Code | Message | +| ---- | ------- | +| 0 | "Transport unknown" +| 1 | "Session ID unknown" +| 2 | "Bad handshake method" +| 3 | "Bad request" +| 4 | "Forbidden" +| 5 | "Unsupported protocol version" + + +##### Properties + +**Important**: if you plan to use Engine.IO in a scalable way, please +keep in mind the properties below will only reflect the clients connected +to a single process. + +- `clients` _(Object)_: hash of connected clients by id. +- `clientsCount` _(Number)_: number of connected clients. + +##### Methods + +- **constructor** + - Initializes the server + - **Parameters** + - `Object`: optional, options object + - **Options** + - `pingTimeout` (`Number`): how many ms without a pong packet to + consider the connection closed (`20000`) + - `pingInterval` (`Number`): how many ms before sending a new ping + packet (`25000`) + - `upgradeTimeout` (`Number`): how many ms before an uncompleted transport upgrade is cancelled (`10000`) + - `maxHttpBufferSize` (`Number`): how many bytes or characters a message + can be, before closing the session (to avoid DoS). Default + value is `1E6`. + - `allowRequest` (`Function`): A function that receives a given handshake + or upgrade request as its first parameter, and can decide whether to + continue or not. The second argument is a function that needs to be + called with the decided information: `fn(err, success)`, where + `success` is a boolean value where false means that the request is + rejected, and err is an error code. + - `transports` (` String`): transports to allow connections + to (`['polling', 'websocket']`) + - `allowUpgrades` (`Boolean`): whether to allow transport upgrades + (`true`) + - `perMessageDeflate` (`Object|Boolean`): parameters of the WebSocket permessage-deflate extension + (see [ws module](https://github.com/einaros/ws) api docs). Set to `true` to enable. (defaults to `false`) + - `threshold` (`Number`): data is compressed only if the byte size is above this value (`1024`) + - `httpCompression` (`Object|Boolean`): parameters of the http compression for the polling transports + (see [zlib](http://nodejs.org/api/zlib.html#zlib_options) api docs). Set to `false` to disable. (`true`) + - `threshold` (`Number`): data is compressed only if the byte size is above this value (`1024`) + - `cookie` (`Object|Boolean`): configuration of the cookie that + contains the client sid to send as part of handshake response + headers. This cookie might be used for sticky-session. Defaults to not sending any cookie (`false`). + See [here](https://github.com/jshttp/cookie#options-1) for all supported options. + - `wsEngine` (`Function`): what WebSocket server implementation to use. Specified module must conform to the `ws` interface (see [ws module api docs](https://github.com/websockets/ws/blob/master/doc/ws.md)). Default value is `ws`. An alternative c++ addon is also available by installing `eiows` module. + - `cors` (`Object`): the options that will be forwarded to the cors module. See [there](https://github.com/expressjs/cors#configuration-options) for all available options. Defaults to no CORS allowed. + - `initialPacket` (`Object`): an optional packet which will be concatenated to the handshake packet emitted by Engine.IO. + - `allowEIO3` (`Boolean`): whether to support v3 Engine.IO clients (defaults to `false`) +- `close` + - Closes all clients + - **Returns** `Server` for chaining +- `handleRequest` + - Called internally when a `Engine` request is intercepted. + - **Parameters** + - `http.IncomingMessage`: a node request object + - `http.ServerResponse`: a node response object + - **Returns** `Server` for chaining +- `handleUpgrade` + - Called internally when a `Engine` ws upgrade is intercepted. + - **Parameters** (same as `upgrade` event) + - `http.IncomingMessage`: a node request object + - `net.Stream`: TCP socket for the request + - `Buffer`: legacy tail bytes + - **Returns** `Server` for chaining +- `attach` + - Attach this Server instance to an `http.Server` + - Captures `upgrade` requests for a `http.Server`. In other words, makes + a regular http.Server WebSocket-compatible. + - **Parameters** + - `http.Server`: server to attach to. + - `Object`: optional, options object + - **Options** + - `path` (`String`): name of the path to capture (`/engine.io`). + - `destroyUpgrade` (`Boolean`): destroy unhandled upgrade requests (`true`) + - `destroyUpgradeTimeout` (`Number`): milliseconds after which unhandled requests are ended (`1000`) +- `generateId` + - Generate a socket id. + - Overwrite this method to generate your custom socket id. + - **Parameters** + - `http.IncomingMessage`: a node request object + - **Returns** A socket id for connected client. + +

+ +#### Socket + +A representation of a client. _Inherits from EventEmitter_. + +##### Events + +- `close` + - Fired when the client is disconnected. + - **Arguments** + - `String`: reason for closing + - `Object`: description object (optional) +- `message` + - Fired when the client sends a message. + - **Arguments** + - `String` or `Buffer`: Unicode string or Buffer with binary contents +- `error` + - Fired when an error occurs. + - **Arguments** + - `Error`: error object +- `upgrading` + - Fired when the client starts the upgrade to a better transport like WebSocket. + - **Arguments** + - `Object`: the transport +- `upgrade` + - Fired when the client completes the upgrade to a better transport like WebSocket. + - **Arguments** + - `Object`: the transport +- `flush` + - Called when the write buffer is being flushed. + - **Arguments** + - `Array`: write buffer +- `drain` + - Called when the write buffer is drained +- `packet` + - Called when a socket received a packet (`message`, `ping`) + - **Arguments** + - `type`: packet type + - `data`: packet data (if type is message) +- `packetCreate` + - Called before a socket sends a packet (`message`, `ping`) + - **Arguments** + - `type`: packet type + - `data`: packet data (if type is message) +- `heartbeat` + - Called when `ping` or `pong` packed is received (depends of client version) + +##### Properties + +- `id` _(String)_: unique identifier +- `server` _(Server)_: engine parent reference +- `request` _(http.IncomingMessage)_: request that originated the Socket +- `upgraded` _(Boolean)_: whether the transport has been upgraded +- `readyState` _(String)_: opening|open|closing|closed +- `transport` _(Transport)_: transport reference + +##### Methods + +- `send`: + - Sends a message, performing `message = toString(arguments[0])` unless + sending binary data, which is sent as is. + - **Parameters** + - `String` | `Buffer` | `ArrayBuffer` | `ArrayBufferView`: a string or any object implementing `toString()`, with outgoing data, or a Buffer or ArrayBuffer with binary data. Also any ArrayBufferView can be sent as is. + - `Object`: optional, options object + - `Function`: optional, a callback executed when the message gets flushed out by the transport + - **Options** + - `compress` (`Boolean`): whether to compress sending data. This option might be ignored and forced to be `true` when using polling. (`true`) + - **Returns** `Socket` for chaining +- `close` + - Disconnects the client + - **Returns** `Socket` for chaining + +### Client + +

+ +Exposed in the `eio` global namespace (in the browser), or by +`require('engine.io-client')` (in Node.JS). + +For the client API refer to the +[engine-client](http://github.com/learnboost/engine.io-client) repository. + +## Debug / logging + +Engine.IO is powered by [debug](http://github.com/visionmedia/debug). +In order to see all the debug output, run your app with the environment variable +`DEBUG` including the desired scope. + +To see the output from all of Engine.IO's debugging scopes you can use: + +``` +DEBUG=engine* node myapp +``` + +## Transports + +- `polling`: XHR / JSONP polling transport. +- `websocket`: WebSocket transport. + +## Plugins + +- [engine.io-conflation](https://github.com/EugenDueck/engine.io-conflation): Makes **conflation and aggregation** of messages straightforward. + +## Support + +The support channels for `engine.io` are the same as `socket.io`: + - irc.freenode.net **#socket.io** + - [Google Groups](http://groups.google.com/group/socket_io) + - [Website](http://socket.io) + +## Development + +To contribute patches, run tests or benchmarks, make sure to clone the +repository: + +``` +git clone git://github.com/LearnBoost/engine.io.git +``` + +Then: + +``` +cd engine.io +npm install +``` + +## Tests + +Tests run with `npm test`. It runs the server tests that are aided by +the usage of `engine.io-client`. + +Make sure `npm install` is run first. + +## Goals + +The main goal of `Engine` is ensuring the most reliable realtime communication. +Unlike the previous Socket.IO core, it always establishes a long-polling +connection first, then tries to upgrade to better transports that are "tested" on +the side. + +During the lifetime of the Socket.IO projects, we've found countless drawbacks +to relying on `HTML5 WebSocket` or `Flash Socket` as the first connection +mechanisms. + +Both are clearly the _right way_ of establishing a bidirectional communication, +with HTML5 WebSocket being the way of the future. However, to answer most business +needs, alternative traditional HTTP 1.1 mechanisms are just as good as delivering +the same solution. + +WebSocket based connections have two fundamental benefits: + +1. **Better server performance** + - _A: Load balancers_
+ Load balancing a long polling connection poses a serious architectural nightmare + since requests can come from any number of open sockets by the user agent, but + they all need to be routed to the process and computer that owns the `Engine` + connection. This negatively impacts RAM and CPU usage. + - _B: Network traffic_
+ WebSocket is designed around the premise that each message frame has to be + surrounded by the least amount of data. In HTTP 1.1 transports, each message + frame is surrounded by HTTP headers and chunked encoding frames. If you try to + send the message _"Hello world"_ with xhr-polling, the message ultimately + becomes larger than if you were to send it with WebSocket. + - _C: Lightweight parser_
+ As an effect of **B**, the server has to do a lot more work to parse the network + data and figure out the message when traditional HTTP requests are used + (as in long polling). This means that another advantage of WebSocket is + less server CPU usage. + +2. **Better user experience** + + Due to the reasons stated in point **1**, the most important effect of being able + to establish a WebSocket connection is raw data transfer speed, which translates + in _some_ cases in better user experience. + + Applications with heavy realtime interaction (such as games) will benefit greatly, + whereas applications like realtime chat (Gmail/Facebook), newsfeeds (Facebook) or + timelines (Twitter) will have negligible user experience improvements. + +Having said this, attempting to establish a WebSocket connection directly so far has +proven problematic: + +1. **Proxies**
+ Many corporate proxies block WebSocket traffic. + +2. **Personal firewall and antivirus software**
+ As a result of our research, we've found that at least 3 personal security + applications block WebSocket traffic. + +3. **Cloud application platforms**
+ Platforms like Heroku or No.de have had trouble keeping up with the fast-paced + nature of the evolution of the WebSocket protocol. Applications therefore end up + inevitably using long polling, but the seamless installation experience of + Socket.IO we strive for (_"require() it and it just works"_) disappears. + +Some of these problems have solutions. In the case of proxies and personal programs, +however, the solutions many times involve upgrading software. Experience has shown +that relying on client software upgrades to deliver a business solution is +fruitless: the very existence of this project has to do with a fragmented panorama +of user agent distribution, with clients connecting with latest versions of the most +modern user agents (Chrome, Firefox and Safari), but others with versions as low as +IE 5.5. + +From the user perspective, an unsuccessful WebSocket connection can translate in +up to at least 10 seconds of waiting for the realtime application to begin +exchanging data. This **perceptively** hurts user experience. + +To summarize, **Engine** focuses on reliability and user experience first, marginal +potential UX improvements and increased server performance second. `Engine` is the +result of all the lessons learned with WebSocket in the wild. + +## Architecture + +The main premise of `Engine`, and the core of its existence, is the ability to +swap transports on the fly. A connection starts as xhr-polling, but it can +switch to WebSocket. + +The central problem this poses is: how do we switch transports without losing +messages? + +`Engine` only switches from polling to another transport in between polling +cycles. Since the server closes the connection after a certain timeout when +there's no activity, and the polling transport implementation buffers messages +in between connections, this ensures no message loss and optimal performance. + +Another benefit of this design is that we workaround almost all the limitations +of **Flash Socket**, such as slow connection times, increased file size (we can +safely lazy load it without hurting user experience), etc. + +## FAQ + +### Can I use engine without Socket.IO ? + +Absolutely. Although the recommended framework for building realtime applications +is Socket.IO, since it provides fundamental features for real-world applications +such as multiplexing, reconnection support, etc. + +`Engine` is to Socket.IO what Connect is to Express. An essential piece for building +realtime frameworks, but something you _probably_ won't be using for building +actual applications. + +### Does the server serve the client? + +No. The main reason is that `Engine` is meant to be bundled with frameworks. +Socket.IO includes `Engine`, therefore serving two clients is not necessary. If +you use Socket.IO, including + +```html + + + + diff --git a/packages/engine.io/examples/latency/index.js b/packages/engine.io/examples/latency/index.js new file mode 100644 index 00000000..db7ec3d4 --- /dev/null +++ b/packages/engine.io/examples/latency/index.js @@ -0,0 +1,34 @@ + +/** + * Module dependencies. + */ + +const express = require('express'); +const app = express(); +const server = require('http').createServer(app); +const enchilada = require('enchilada'); +const io = require('engine.io').attach(server); + +app.use(enchilada({ + src: __dirname + '/public', + debug: true +})); +app.use(express.static(__dirname + '/public')); +app.get('/', (req, res) => { + res.sendFile(__dirname + '/index.html'); +}); + +app.get('/engine.io.min.js', (req, res) => { + res.sendFile(require.resolve('engine.io-client/dist/engine.io.min.js')); +}); + +io.on('connection', (socket) => { + socket.on('message', () => { + socket.send('pong'); + }); +}); + +const port = process.env.PORT || 3000; +server.listen(port, () => { + console.log('\x1B[96mlistening on localhost:' + port + ' \x1B[39m'); +}); diff --git a/packages/engine.io/examples/latency/package-lock.json b/packages/engine.io/examples/latency/package-lock.json new file mode 100644 index 00000000..407cb591 --- /dev/null +++ b/packages/engine.io/examples/latency/package-lock.json @@ -0,0 +1,1708 @@ +{ + "name": "eio-latency", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "18.16.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.3.tgz", + "integrity": "sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==" + }, + "Base64": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", + "integrity": "sha1-ujpCMHCOGGcFBl5mur3Uw1z2ACg=" + }, + "JSONStream": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.7.4.tgz", + "integrity": "sha1-c0KQ5BUR7qfCz+FR+/mlY6l7l4Y=", + "requires": { + "jsonparse": "0.0.5", + "through": ">=2.2.7 <3" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "dependencies": { + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + } + } + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "assert": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.1.2.tgz", + "integrity": "sha1-raoExGu1jG3R8pTaPrJuYijrbkQ=", + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "astw": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz", + "integrity": "sha1-e9QXhNMkk5h66yOba04cV6hzuRc=", + "requires": { + "acorn": "^4.0.3" + } + }, + "async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==" + }, + "base64-js": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.7.tgz", + "integrity": "sha1-VEANyR1pbOwyqKR5AvlxUi/uj0g=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "browser-pack": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-2.0.1.tgz", + "integrity": "sha1-XRxSf1bFgmd0EcTbKhKGSP9r8VA=", + "requires": { + "JSONStream": "~0.6.4", + "combine-source-map": "~0.3.0", + "through": "~2.3.4" + }, + "dependencies": { + "JSONStream": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.6.4.tgz", + "integrity": "sha1-SyyAY/j1Enh7I3X37p22kgj6Lcs=", + "requires": { + "jsonparse": "0.0.5", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=" + } + } + } + } + }, + "browser-resolve": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.2.4.tgz", + "integrity": "sha1-Wa54IKgpVezTL1+3xGisIcRyOAY=", + "requires": { + "resolve": "0.6.3" + }, + "dependencies": { + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=" + } + } + }, + "browserify": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-4.1.9.tgz", + "integrity": "sha1-xKapETse1HgSNQQxTWVunkZpJps=", + "requires": { + "JSONStream": "~0.7.1", + "assert": "~1.1.0", + "browser-pack": "~2.0.0", + "browser-resolve": "~1.2.1", + "browserify-zlib": "~0.1.2", + "buffer": "^2.3.0", + "builtins": "~0.0.3", + "commondir": "0.0.1", + "concat-stream": "~1.4.1", + "console-browserify": "~1.0.1", + "constants-browserify": "~0.0.1", + "crypto-browserify": "~1.0.9", + "deep-equal": "~0.1.0", + "defined": "~0.0.0", + "deps-sort": "~0.1.1", + "derequire": "~0.8.0", + "domain-browser": "~1.1.0", + "duplexer": "~0.1.1", + "events": "~1.0.0", + "glob": "~3.2.8", + "http-browserify": "~1.3.1", + "https-browserify": "~0.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "~6.0.0", + "module-deps": "~2.1.1", + "os-browserify": "~0.1.1", + "parents": "~0.0.1", + "path-browserify": "~0.0.0", + "process": "^0.7.0", + "punycode": "~1.2.3", + "querystring-es3": "~0.2.0", + "readable-stream": "^1.0.27-1", + "resolve": "~0.7.1", + "shallow-copy": "0.0.1", + "shell-quote": "~0.0.1", + "stream-browserify": "^1.0.0", + "stream-combiner": "~0.0.2", + "string_decoder": "~0.0.0", + "subarg": "0.0.1", + "syntax-error": "~1.1.0", + "through2": "~0.4.1", + "timers-browserify": "~1.0.1", + "tty-browserify": "~0.0.0", + "umd": "~2.1.0", + "url": "~0.10.1", + "util": "~0.10.1", + "vm-browserify": "~0.0.1", + "xtend": "^3.0.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "requires": { + "pako": "~0.2.0" + } + }, + "buffer": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-2.8.3.tgz", + "integrity": "sha512-dyatqxbSWlkhnG5lthQ7TDh2NfShsKesnKiGyt5DmiJfvKJ1zBq1AvC3+neSY565BziAiYwbothV2tizAr2WRg==", + "requires": { + "base64-js": "0.0.7", + "ieee754": "^1.1.4", + "is-array": "^1.0.1" + } + }, + "builtins": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-0.0.7.tgz", + "integrity": "sha1-NVIZzWzxjb58Acx/0tznZc/cVJo=" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "combine-source-map": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.3.0.tgz", + "integrity": "sha1-2edPWT2c1DgHMSy12EbUUe+qnrc=", + "requires": { + "convert-source-map": "~0.3.0", + "inline-source-map": "~0.3.0", + "source-map": "~0.1.31" + }, + "dependencies": { + "convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=" + } + } + }, + "commondir": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-0.0.1.tgz", + "integrity": "sha1-ifAP3NUbUZxXhzP+xWPmptp/W+I=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-stream": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.11.tgz", + "integrity": "sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.9", + "typedarray": "~0.0.5" + } + }, + "console-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.0.3.tgz", + "integrity": "sha1-04mNLDqTEC82QZf4h0tPkrUoao4=" + }, + "constants-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-0.0.1.tgz", + "integrity": "sha1-kld9tSe6bEzwpFaNhLwDH0QeIfI=" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "convert-source-map": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.1.tgz", + "integrity": "sha1-dOUYJHMFhBOwkN1zd3rLxKD/88w=" + }, + "cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "crypto-browserify": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz", + "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=" + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha1-skbCuApXCkfBG+HZvRBw7IeLh84=" + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "deps-sort": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-0.1.2.tgz", + "integrity": "sha1-2qL7YUoXyWN9gB4vVTOa43DzYRo=", + "requires": { + "JSONStream": "~0.6.4", + "minimist": "~0.0.1", + "through": "~2.3.4" + }, + "dependencies": { + "JSONStream": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.6.4.tgz", + "integrity": "sha1-SyyAY/j1Enh7I3X37p22kgj6Lcs=", + "requires": { + "jsonparse": "0.0.5", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=" + } + } + } + } + }, + "derequire": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/derequire/-/derequire-0.8.0.tgz", + "integrity": "sha1-wffx2izt5Ere3gRzePA/RE6cTA0=", + "requires": { + "esprima-fb": "^3001.1.0-dev-harmony-fb", + "esrefactor": "~0.1.0", + "estraverse": "~1.5.0" + } + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detective": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-3.1.0.tgz", + "integrity": "sha1-d3gkRKt1K4jKG+Lp0KA5Xx2iXu0=", + "requires": { + "escodegen": "~1.1.0", + "esprima-fb": "3001.1.0-dev-harmony-fb" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "requires": { + "readable-stream": "~1.1.9" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "enchilada": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/enchilada/-/enchilada-0.13.0.tgz", + "integrity": "sha1-Sp5ZqJCwWN0qJ3ukozv763xOwQg=", + "requires": { + "browserify": "4.1.9", + "convert-source-map": "1.1.1", + "debug": "2.2.0", + "filewatcher": "3.0.0", + "mime": "1.2.11", + "ready-signal": "1.3.0", + "uglify-js": "2.5.0" + } + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "engine.io": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.2.tgz", + "integrity": "sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.11.0" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "engine.io-parser": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==" + } + } + }, + "engine.io-client": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.4.tgz", + "integrity": "sha512-843fqAdKeUMFqKi1sSjnR11tJ4wi8sIefu6+JC1OzkkJBmjtc/gM/rZ53tJfu5Iae/3gApm5veoS+v+gtT0+Fg==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.1.tgz", + "integrity": "sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg==" + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escodegen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", + "integrity": "sha1-xmOSP24gqtSNDA+knzHG1PSTYM8=", + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + } + } + }, + "escope": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/escope/-/escope-0.0.16.tgz", + "integrity": "sha1-QYx6CvynIdr+ZZGT/Zhig+dGU48=", + "requires": { + "estraverse": ">= 0.0.2" + } + }, + "esprima-fb": { + "version": "3001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", + "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=" + }, + "esrefactor": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/esrefactor/-/esrefactor-0.1.0.tgz", + "integrity": "sha1-0UJ5WigjOauB6Ta1t6IbEb8ZexM=", + "requires": { + "escope": "~0.0.13", + "esprima": "~1.0.2", + "estraverse": "~0.0.4" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-0.0.4.tgz", + "integrity": "sha1-AaCTLf7ldGhKWYr1pnw7+bZCjbI=" + } + } + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "events": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/events/-/events-1.0.2.tgz", + "integrity": "sha1-dYSdz+k9EPsFfDAFWv29UdBqjiQ=" + }, + "express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "filewatcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filewatcher/-/filewatcher-3.0.0.tgz", + "integrity": "sha1-/HYcHLkG9xWHppu8G1vTiQxQ0P8=", + "requires": { + "debounce": "^1.0.0" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==" + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-browserify": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.3.2.tgz", + "integrity": "sha1-tWLDRHk0mmkNemWX30la76jGBPU=", + "requires": { + "Base64": "~0.2.0", + "inherits": "~2.0.1" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inline-source-map": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.3.1.tgz", + "integrity": "sha1-pSi1FOaJ/OkNswiehw2S9Sestes=", + "requires": { + "source-map": "~0.3.0" + }, + "dependencies": { + "source-map": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.3.0.tgz", + "integrity": "sha1-hYb7mloAXltQHiHNGLbyG0V60fk=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "insert-module-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-6.0.0.tgz", + "integrity": "sha1-7orrne4WgZ4zqhRYilWIJK8MFdw=", + "requires": { + "JSONStream": "~0.7.1", + "concat-stream": "~1.4.1", + "lexical-scope": "~1.1.0", + "process": "~0.6.0", + "through": "~2.3.4", + "xtend": "^3.0.0" + }, + "dependencies": { + "process": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.6.0.tgz", + "integrity": "sha1-fdm+gP+q7dTLYo8YJ/HLq23AkY8=" + } + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-array/-/is-array-1.0.1.tgz", + "integrity": "sha1-6YUMwsyGDDvAl36EzPDdRkWEJ5o=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "jsonparse": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz", + "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=" + }, + "lexical-scope": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lexical-scope/-/lexical-scope-1.1.1.tgz", + "integrity": "sha1-3rrBBnQ18TWdkPz9npS8su5Hsr8=", + "requires": { + "astw": "^2.0.0" + } + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "module-deps": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-2.1.5.tgz", + "integrity": "sha1-N1qbyATM1kzrs8Yu5kN1Xws8zyk=", + "requires": { + "JSONStream": "~0.7.1", + "browser-resolve": "~1.2.4", + "concat-stream": "~1.4.5", + "detective": "~3.1.0", + "duplexer2": "0.0.2", + "inherits": "~2.0.1", + "minimist": "~0.0.9", + "parents": "0.0.2", + "readable-stream": "^1.0.27-1", + "resolve": "~0.6.3", + "stream-combiner": "~0.1.0", + "subarg": "0.0.1", + "through2": "~0.4.1" + }, + "dependencies": { + "parents": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/parents/-/parents-0.0.2.tgz", + "integrity": "sha1-ZxR4JuSX1AdZqvW6TJllm2A00wI=" + }, + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=" + }, + "stream-combiner": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.1.0.tgz", + "integrity": "sha1-DcOJo8ID+PTVY2j5Xd5S65Jptb4=", + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + } + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "requires": { + "wordwrap": "~0.0.2" + } + }, + "os-browserify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz", + "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parents": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/parents/-/parents-0.0.3.tgz", + "integrity": "sha1-+iEvAk2fpjGNu2tM5nbIvkk7nEM=", + "requires": { + "path-platform": "^0.0.1" + } + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + }, + "path-platform": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.0.1.tgz", + "integrity": "sha1-tVhdfDxGPYmqAGDYZhHPGv1hfio=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "process": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.7.0.tgz", + "integrity": "sha1-xSIIFho0rfOBI0SuhdPmFQRpOJ0=" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "punycode": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.2.4.tgz", + "integrity": "sha1-VACKyXKux0F13vnLpt9/qdORh0A=" + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + }, + "dependencies": { + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "ready-signal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ready-signal/-/ready-signal-1.3.0.tgz", + "integrity": "sha1-VUA/kg4eHyqLBloii5pj77Cjwts=" + }, + "resolve": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.7.4.tgz", + "integrity": "sha1-OVqe+ehz+/4SvRRAi9kbuTYAPWk=" + }, + "rfile": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rfile/-/rfile-1.0.0.tgz", + "integrity": "sha1-WXCM+Qyh50xUw8/Fw2/bmBBDUmE=", + "requires": { + "callsite": "~1.0.0", + "resolve": "~0.3.0" + }, + "dependencies": { + "resolve": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.3.1.tgz", + "integrity": "sha1-NMY0R8ZkxwWY0cmxJvxDsqJDEKQ=" + } + } + }, + "ruglify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ruglify/-/ruglify-1.0.0.tgz", + "integrity": "sha1-3Ikw4qlUSidDAcyZcldMDQmGtnU=", + "requires": { + "rfile": "~1.0", + "uglify-js": "~2.2" + }, + "dependencies": { + "uglify-js": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", + "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", + "requires": { + "optimist": "~0.3.5", + "source-map": "~0.1.7" + } + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, + "shell-quote": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-0.0.1.tgz", + "integrity": "sha1-GkEZbzwDM8SCMjWT1ohuzxU92YY=" + }, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" + }, + "smoothie": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/smoothie/-/smoothie-1.19.0.tgz", + "integrity": "sha1-kVZqT9PFES3CrJWAAQT6Ruz7XBA=" + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "stream-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-1.0.0.tgz", + "integrity": "sha1-v5tKv7QrJ011FHnkTg/yZWtvEZM=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^1.0.27-1" + } + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "~0.1.1" + } + }, + "string_decoder": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.0.1.tgz", + "integrity": "sha1-9UctCo0WUOyCN1LSTm/WJ7Ob8UE=" + }, + "subarg": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-0.0.1.tgz", + "integrity": "sha1-PVawfaz7xFu7Y/dnK0O2PkY2jjo=", + "requires": { + "minimist": "~0.0.7" + } + }, + "syntax-error": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.1.6.tgz", + "integrity": "sha1-tFSXBtOGzBwdx8JCPxhXm2yt5xA=", + "requires": { + "acorn": "^2.7.0" + }, + "dependencies": { + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "timers-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.0.3.tgz", + "integrity": "sha1-/7pwycEu7ZFv1nMY5imsbzIpVVE=", + "requires": { + "process": "~0.5.1" + }, + "dependencies": { + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" + } + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "uglify-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.5.0.tgz", + "integrity": "sha1-SrXWWkcw7Lek+2LT9JniBU2Y+6E=", + "requires": { + "async": "~0.2.6", + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" + }, + "umd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/umd/-/umd-2.1.0.tgz", + "integrity": "sha1-SmMHt2LxfwLSAbX6FU5nM5bCY88=", + "requires": { + "rfile": "~1.0.0", + "ruglify": "~1.0.0", + "through": "~2.3.4", + "uglify-js": "~2.4.0" + }, + "dependencies": { + "source-map": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "uglify-js": { + "version": "2.4.24", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=", + "requires": { + "async": "~0.2.6", + "source-map": "0.1.34", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.5.4" + } + } + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + }, + "xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==" + }, + "xtend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", + "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" + }, + "yargs": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=", + "requires": { + "camelcase": "^1.0.2", + "decamelize": "^1.0.0", + "window-size": "0.1.0", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==" + } + } +} diff --git a/packages/engine.io/examples/latency/package.json b/packages/engine.io/examples/latency/package.json new file mode 100644 index 00000000..6737cb2b --- /dev/null +++ b/packages/engine.io/examples/latency/package.json @@ -0,0 +1,11 @@ +{ + "name": "eio-latency", + "version": "0.1.0", + "dependencies": { + "enchilada": "0.13.0", + "engine.io": "^6.4.2", + "engine.io-client": "^4.1.4", + "express": "^4.19.2", + "smoothie": "1.19.0" + } +} diff --git a/packages/engine.io/examples/latency/public/index.js b/packages/engine.io/examples/latency/public/index.js new file mode 100644 index 00000000..ea74058e --- /dev/null +++ b/packages/engine.io/examples/latency/public/index.js @@ -0,0 +1,58 @@ + +/** + * Module dependencies. + */ + +const SmoothieChart = require('smoothie').SmoothieChart; +const TimeSeries = require('smoothie').TimeSeries; + +// helper + +function $ (id) { return document.getElementById(id); } + +// chart + +let smoothie; +let time; + +function render () { + if (smoothie) smoothie.stop(); + $('chart').width = document.body.clientWidth; + smoothie = new SmoothieChart(); + smoothie.streamTo($('chart'), 1000); + time = new TimeSeries(); + smoothie.addTimeSeries(time, { + strokeStyle: 'rgb(255, 0, 0)', + fillStyle: 'rgba(255, 0, 0, 0.4)', + lineWidth: 2 + }); +} + +// socket +const socket = new eio.Socket(); +let last; +function send () { + last = new Date(); + socket.send('ping'); + $('transport').innerHTML = socket.transport.name; +} + +socket.on('open', () => { + if ($('chart').getContext) { + render(); + window.onresize = render; + } + send(); +}); + +socket.on('close', () => { + if (smoothie) smoothie.stop(); + $('transport').innerHTML = '(disconnected)'; +}); + +socket.on('message', () => { + const latency = new Date() - last; + $('latency').innerHTML = latency + 'ms'; + if (time) time.append(+new Date(), latency); + setTimeout(send, 100); +}); diff --git a/packages/engine.io/examples/latency/public/style.css b/packages/engine.io/examples/latency/public/style.css new file mode 100644 index 00000000..ab61214c --- /dev/null +++ b/packages/engine.io/examples/latency/public/style.css @@ -0,0 +1,5 @@ + +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/packages/engine.io/examples/memory-usage-webtransport/.gitignore b/packages/engine.io/examples/memory-usage-webtransport/.gitignore new file mode 100755 index 00000000..cfb6da4f --- /dev/null +++ b/packages/engine.io/examples/memory-usage-webtransport/.gitignore @@ -0,0 +1,2 @@ +*.pem +*.log diff --git a/packages/engine.io/examples/memory-usage-webtransport/client.js b/packages/engine.io/examples/memory-usage-webtransport/client.js new file mode 100644 index 00000000..db81dc19 --- /dev/null +++ b/packages/engine.io/examples/memory-usage-webtransport/client.js @@ -0,0 +1,35 @@ +import { Socket } from "engine.io-client"; +import { X509Certificate } from "crypto"; +import { readFileSync } from "node:fs"; +import { WebTransport } from "@fails-components/webtransport"; + +const cert = readFileSync("./cert.pem"); +const CLIENTS_COUNT = 100; + +global.WebTransport = WebTransport; + +for (let i = 0; i < CLIENTS_COUNT; i++) { + const socket = new Socket("ws://localhost:3000", { + transports: ["webtransport"], + transportOptions: { + webtransport: { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: Buffer.from( + new X509Certificate(cert).fingerprint256 + .split(":") + .map((el) => parseInt(el, 16)) + ), + }, + ], + }, + }, + }); + + socket.on("open", () => {}); + + socket.on("message", () => {}); + + socket.on("close", (reason) => {}); +} diff --git a/packages/engine.io/examples/memory-usage-webtransport/generate_cert.sh b/packages/engine.io/examples/memory-usage-webtransport/generate_cert.sh new file mode 100755 index 00000000..bbc1e6ea --- /dev/null +++ b/packages/engine.io/examples/memory-usage-webtransport/generate_cert.sh @@ -0,0 +1,6 @@ +#!/bin/bash +openssl req -new -x509 -nodes \ + -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -days 14 \ + -out cert.pem -keyout key.pem \ + -subj '/CN=127.0.0.1' diff --git a/packages/engine.io/examples/memory-usage-webtransport/package-lock.json b/packages/engine.io/examples/memory-usage-webtransport/package-lock.json new file mode 100644 index 00000000..99f3318c --- /dev/null +++ b/packages/engine.io/examples/memory-usage-webtransport/package-lock.json @@ -0,0 +1,530 @@ +{ + "name": "memory-usage-webtransport", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "memory-usage-webtransport", + "version": "0.0.1", + "dependencies": { + "@fails-components/webtransport": "^0.3.1", + "engine.io-client": "^6.5.3" + } + }, + "node_modules/@fails-components/webtransport": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@fails-components/webtransport/-/webtransport-0.3.1.tgz", + "integrity": "sha512-pbFhPSDCg1vLXiUMq3Q9D9O/tI8g4KpuPFAnPREAYoOw/wU4f/U4e93g0wnxIKyrmnz0ZM7lzf3VMJuHP8SzYw==", + "hasInstallScript": true, + "dependencies": { + "@types/debug": "^4.1.7", + "bindings": "^1.5.0", + "debug": "^4.3.4", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": ">=16.5" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/node-abi": { + "version": "3.65.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.65.0.tgz", + "integrity": "sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/packages/engine.io/examples/memory-usage-webtransport/package.json b/packages/engine.io/examples/memory-usage-webtransport/package.json new file mode 100644 index 00000000..5f0b7bdd --- /dev/null +++ b/packages/engine.io/examples/memory-usage-webtransport/package.json @@ -0,0 +1,11 @@ +{ + "name": "memory-usage-webtransport", + "version": "0.0.1", + "private": true, + "description": "", + "type": "module", + "dependencies": { + "@fails-components/webtransport": "^0.3.1", + "engine.io-client": "^6.5.3" + } +} diff --git a/packages/engine.io/examples/memory-usage-webtransport/server.js b/packages/engine.io/examples/memory-usage-webtransport/server.js new file mode 100644 index 00000000..2128ec38 --- /dev/null +++ b/packages/engine.io/examples/memory-usage-webtransport/server.js @@ -0,0 +1,65 @@ +import { readFileSync } from "node:fs"; +import { Http3Server } from "@fails-components/webtransport"; +import { Server } from "../../build/engine.io.js"; + +const key = readFileSync("./key.pem"); +const cert = readFileSync("./cert.pem"); + +const PACKETS_COUNT = 100; +const PACKET_SIZE = 100; + +const engine = new Server({ + transports: ["polling", "websocket", "webtransport"], +}); + +const h3Server = new Http3Server({ + port: 3000, + host: "0.0.0.0", + secret: "changeit", + cert, + privKey: key, +}); + +const packets = []; + +for (let i = 0; i < PACKETS_COUNT; i++) { + packets.push("a".repeat(PACKET_SIZE)); +} + +setInterval(() => { + Object.keys(engine.clients).forEach((id) => { + const client = engine.clients[id]; + packets.forEach((packet) => { + client.send(packet); + }); + }); +}, 10000); + +function formatSize(val) { + return Math.floor(val / 1024); +} + +setInterval(() => { + const mem = process.memoryUsage(); + console.log( + `${Math.floor(process.uptime())}; ${formatSize(mem.heapUsed)}; ${formatSize( + mem.heapTotal + )}` + ); +}, 1000); + +h3Server.startServer(); + +(async () => { + // const stream = await h3Server.sessionStream("/engine.io/"); + const stream = await h3Server.sessionStream("/engine.io/", {}); + const sessionReader = stream.getReader(); + + while (true) { + const { done, value } = await sessionReader.read(); + if (done) { + break; + } + engine.onWebTransportSession(value); + } +})(); diff --git a/packages/engine.io/examples/memory-usage/.gitignore b/packages/engine.io/examples/memory-usage/.gitignore new file mode 100755 index 00000000..397b4a76 --- /dev/null +++ b/packages/engine.io/examples/memory-usage/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/packages/engine.io/examples/memory-usage/client.js b/packages/engine.io/examples/memory-usage/client.js new file mode 100644 index 00000000..90a1ebea --- /dev/null +++ b/packages/engine.io/examples/memory-usage/client.js @@ -0,0 +1,15 @@ +import { Socket } from "engine.io-client"; + +const CLIENTS_COUNT = 100; + +for (let i = 0; i < CLIENTS_COUNT; i++) { + const socket = new Socket("ws://localhost:3000", { + transports: ["websocket"], + }); + + socket.on("open", () => {}); + + socket.on("message", () => {}); + + socket.on("close", (reason) => {}); +} diff --git a/packages/engine.io/examples/memory-usage/package-lock.json b/packages/engine.io/examples/memory-usage/package-lock.json new file mode 100644 index 00000000..1acf8dea --- /dev/null +++ b/packages/engine.io/examples/memory-usage/package-lock.json @@ -0,0 +1,89 @@ +{ + "name": "memory-usage", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "memory-usage", + "version": "0.0.1", + "dependencies": { + "engine.io-client": "^6.5.4" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/packages/engine.io/examples/memory-usage/package.json b/packages/engine.io/examples/memory-usage/package.json new file mode 100644 index 00000000..030c8769 --- /dev/null +++ b/packages/engine.io/examples/memory-usage/package.json @@ -0,0 +1,10 @@ +{ + "name": "memory-usage", + "version": "0.0.1", + "private": true, + "description": "", + "type": "module", + "dependencies": { + "engine.io-client": "^6.5.4" + } +} diff --git a/packages/engine.io/examples/memory-usage/server.js b/packages/engine.io/examples/memory-usage/server.js new file mode 100644 index 00000000..c051220b --- /dev/null +++ b/packages/engine.io/examples/memory-usage/server.js @@ -0,0 +1,41 @@ +import { createServer } from "node:http"; +import { Server } from "../../build/engine.io.js"; + +const EMIT_INTERVAL_MS = 10_000; +const PACKETS_COUNT = 100; +const PACKET_SIZE = 100; + +const httpServer = createServer(); +const engine = new Server(); + +engine.attach(httpServer); + +const packets = []; + +for (let i = 0; i < PACKETS_COUNT; i++) { + packets.push("a".repeat(PACKET_SIZE)); +} + +setInterval(() => { + Object.keys(engine.clients).forEach((id) => { + const client = engine.clients[id]; + packets.forEach((packet) => { + client.send(packet); + }); + }); +}, EMIT_INTERVAL_MS); + +function formatSize(val) { + return Math.floor(val / 1024); +} + +setInterval(() => { + const mem = process.memoryUsage(); + console.log( + `${Math.floor(process.uptime())}; ${formatSize(mem.heapUsed)}; ${formatSize( + mem.heapTotal + )}` + ); +}, 1000); + +httpServer.listen(3000); diff --git a/packages/engine.io/lib/engine.io.ts b/packages/engine.io/lib/engine.io.ts new file mode 100644 index 00000000..3ba9e67f --- /dev/null +++ b/packages/engine.io/lib/engine.io.ts @@ -0,0 +1,54 @@ +import { createServer } from "http"; +import { Server, AttachOptions, ServerOptions } from "./server"; +import transports from "./transports/index"; +import * as parser from "engine.io-parser"; + +export { Server, transports, listen, attach, parser }; +export type { AttachOptions, ServerOptions, BaseServer } from "./server"; +export { uServer } from "./userver"; +export { Socket } from "./socket"; +export { Transport } from "./transport"; +export const protocol = parser.protocol; + +/** + * Creates an http.Server exclusively used for WS upgrades. + * + * @param {Number} port + * @param {Function} callback + * @param {Object} options + * @return {Server} websocket.io server + */ + +function listen(port, options: AttachOptions & ServerOptions, fn) { + if ("function" === typeof options) { + fn = options; + options = {}; + } + + const server = createServer(function (req, res) { + res.writeHead(501); + res.end("Not Implemented"); + }); + + // create engine server + const engine = attach(server, options); + engine.httpServer = server; + + server.listen(port, fn); + + return engine; +} + +/** + * Captures upgrade requests for a http.Server. + * + * @param {http.Server} server + * @param {Object} options + * @return {Server} engine server + */ + +function attach(server, options: AttachOptions & ServerOptions) { + const engine = new Server(options); + engine.attach(server, options); + return engine; +} diff --git a/packages/engine.io/lib/parser-v3/index.ts b/packages/engine.io/lib/parser-v3/index.ts new file mode 100644 index 00000000..367ae137 --- /dev/null +++ b/packages/engine.io/lib/parser-v3/index.ts @@ -0,0 +1,485 @@ +// imported from https://github.com/socketio/engine.io-parser/tree/2.2.x + +/** + * Module dependencies. + */ + +var utf8 = require('./utf8'); + +/** + * Current protocol version. + */ +export const protocol = 3; + +const hasBinary = (packets) => { + for (const packet of packets) { + if (packet.data instanceof ArrayBuffer || ArrayBuffer.isView(packet.data)) { + return true; + } + } + return false; +} + +/** + * Packet types. + */ + +export const packets = { + open: 0 // non-ws + , close: 1 // non-ws + , ping: 2 + , pong: 3 + , message: 4 + , upgrade: 5 + , noop: 6 +}; + +var packetslist = Object.keys(packets); + +/** + * Premade error packet. + */ + +var err = { type: 'error', data: 'parser error' }; + +const EMPTY_BUFFER = Buffer.concat([]); + +/** + * Encodes a packet. + * + * [ ] + * + * Example: + * + * 5hello world + * 3 + * 4 + * + * Binary is encoded in an identical principle + * + * @api private + */ + +export function encodePacket (packet, supportsBinary, utf8encode, callback) { + if (typeof supportsBinary === 'function') { + callback = supportsBinary; + supportsBinary = null; + } + + if (typeof utf8encode === 'function') { + callback = utf8encode; + utf8encode = null; + } + + if (Buffer.isBuffer(packet.data)) { + return encodeBuffer(packet, supportsBinary, callback); + } else if (packet.data && (packet.data.buffer || packet.data) instanceof ArrayBuffer) { + return encodeBuffer({ type: packet.type, data: arrayBufferToBuffer(packet.data) }, supportsBinary, callback); + } + + // Sending data as a utf-8 string + var encoded = packets[packet.type]; + + // data fragment is optional + if (undefined !== packet.data) { + encoded += utf8encode ? utf8.encode(String(packet.data), { strict: false }) : String(packet.data); + } + + return callback('' + encoded); +}; + +/** + * Encode Buffer data + */ + +function encodeBuffer(packet, supportsBinary, callback) { + if (!supportsBinary) { + return encodeBase64Packet(packet, callback); + } + + var data = packet.data; + var typeBuffer = Buffer.allocUnsafe(1); + typeBuffer[0] = packets[packet.type]; + return callback(Buffer.concat([typeBuffer, data])); +} + +/** + * Encodes a packet with binary data in a base64 string + * + * @param {Object} packet, has `type` and `data` + * @return {String} base64 encoded message + */ + +export function encodeBase64Packet (packet, callback){ + var data = Buffer.isBuffer(packet.data) ? packet.data : arrayBufferToBuffer(packet.data); + var message = 'b' + packets[packet.type]; + message += data.toString('base64'); + return callback(message); +}; + +/** + * Decodes a packet. Data also available as an ArrayBuffer if requested. + * + * @return {Object} with `type` and `data` (if any) + * @api private + */ + +export function decodePacket (data, binaryType, utf8decode) { + if (data === undefined) { + return err; + } + + var type; + + // String data + if (typeof data === 'string') { + + type = data.charAt(0); + + if (type === 'b') { + return decodeBase64Packet(data.slice(1), binaryType); + } + + if (utf8decode) { + data = tryDecode(data); + if (data === false) { + return err; + } + } + + if (Number(type) != type || !packetslist[type]) { + return err; + } + + if (data.length > 1) { + return { type: packetslist[type], data: data.slice(1) }; + } else { + return { type: packetslist[type] }; + } + } + + // Binary data + if (binaryType === 'arraybuffer') { + // wrap Buffer/ArrayBuffer data into an Uint8Array + var intArray = new Uint8Array(data); + type = intArray[0]; + return { type: packetslist[type], data: intArray.buffer.slice(1) }; + } + + if (data instanceof ArrayBuffer) { + data = arrayBufferToBuffer(data); + } + type = data[0]; + return { type: packetslist[type], data: data.slice(1) }; +}; + +function tryDecode(data) { + try { + data = utf8.decode(data, { strict: false }); + } catch (e) { + return false; + } + return data; +} + +/** + * Decodes a packet encoded in a base64 string. + * + * @param {String} base64 encoded message + * @return {Object} with `type` and `data` (if any) + */ + +export function decodeBase64Packet (msg, binaryType) { + var type = packetslist[msg.charAt(0)]; + var data = Buffer.from(msg.slice(1), 'base64'); + if (binaryType === 'arraybuffer') { + var abv = new Uint8Array(data.length); + for (var i = 0; i < abv.length; i++){ + abv[i] = data[i]; + } + // @ts-ignore + data = abv.buffer; + } + return { type: type, data: data }; +}; + +/** + * Encodes multiple messages (payload). + * + * :data + * + * Example: + * + * 11:hello world2:hi + * + * If any contents are binary, they will be encoded as base64 strings. Base64 + * encoded strings are marked with a b before the length specifier + * + * @param {Array} packets + * @api private + */ + +export function encodePayload (packets, supportsBinary, callback) { + if (typeof supportsBinary === 'function') { + callback = supportsBinary; + supportsBinary = null; + } + + if (supportsBinary && hasBinary(packets)) { + return encodePayloadAsBinary(packets, callback); + } + + if (!packets.length) { + return callback('0:'); + } + + function encodeOne(packet, doneCallback) { + encodePacket(packet, supportsBinary, false, function(message) { + doneCallback(null, setLengthHeader(message)); + }); + } + + map(packets, encodeOne, function(err, results) { + return callback(results.join('')); + }); +}; + +function setLengthHeader(message) { + return message.length + ':' + message; +} + +/** + * Async array map using after + */ + +function map(ary, each, done) { + const results = new Array(ary.length); + let count = 0; + + for (let i = 0; i < ary.length; i++) { + each(ary[i], (error, msg) => { + results[i] = msg; + if (++count === ary.length) { + done(null, results); + } + }); + } +} + +/* + * Decodes data when a payload is maybe expected. Possible binary contents are + * decoded from their base64 representation + * + * @param {String} data, callback method + * @api public + */ + +export function decodePayload (data, binaryType, callback) { + if (typeof data !== 'string') { + return decodePayloadAsBinary(data, binaryType, callback); + } + + if (typeof binaryType === 'function') { + callback = binaryType; + binaryType = null; + } + + if (data === '') { + // parser error - ignoring payload + return callback(err, 0, 1); + } + + var length = '', n, msg, packet; + + for (var i = 0, l = data.length; i < l; i++) { + var chr = data.charAt(i); + + if (chr !== ':') { + length += chr; + continue; + } + + // @ts-ignore + if (length === '' || (length != (n = Number(length)))) { + // parser error - ignoring payload + return callback(err, 0, 1); + } + + msg = data.slice(i + 1, i + 1 + n); + + if (length != msg.length) { + // parser error - ignoring payload + return callback(err, 0, 1); + } + + if (msg.length) { + packet = decodePacket(msg, binaryType, false); + + if (err.type === packet.type && err.data === packet.data) { + // parser error in individual packet - ignoring payload + return callback(err, 0, 1); + } + + var more = callback(packet, i + n, l); + if (false === more) return; + } + + // advance cursor + i += n; + length = ''; + } + + if (length !== '') { + // parser error - ignoring payload + return callback(err, 0, 1); + } + +}; + +/** + * + * Converts a buffer to a utf8.js encoded string + * + * @api private + */ + +function bufferToString(buffer) { + var str = ''; + for (var i = 0, l = buffer.length; i < l; i++) { + str += String.fromCharCode(buffer[i]); + } + return str; +} + +/** + * + * Converts a utf8.js encoded string to a buffer + * + * @api private + */ + +function stringToBuffer(string) { + var buf = Buffer.allocUnsafe(string.length); + for (var i = 0, l = string.length; i < l; i++) { + buf.writeUInt8(string.charCodeAt(i), i); + } + return buf; +} + +/** + * + * Converts an ArrayBuffer to a Buffer + * + * @api private + */ + +function arrayBufferToBuffer(data) { + // data is either an ArrayBuffer or ArrayBufferView. + var length = data.byteLength || data.length; + var offset = data.byteOffset || 0; + + return Buffer.from(data.buffer || data, offset, length); +} + +/** + * Encodes multiple messages (payload) as binary. + * + * <1 = binary, 0 = string>[...] + * + * Example: + * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers + * + * @param {Array} packets + * @return {Buffer} encoded payload + * @api private + */ + +export function encodePayloadAsBinary (packets, callback) { + if (!packets.length) { + return callback(EMPTY_BUFFER); + } + + map(packets, encodeOneBinaryPacket, function(err, results) { + return callback(Buffer.concat(results)); + }); +}; + +function encodeOneBinaryPacket(p, doneCallback) { + + function onBinaryPacketEncode(packet) { + + var encodingLength = '' + packet.length; + var sizeBuffer; + + if (typeof packet === 'string') { + sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2); + sizeBuffer[0] = 0; // is a string (not true binary = 0) + for (var i = 0; i < encodingLength.length; i++) { + sizeBuffer[i + 1] = parseInt(encodingLength[i], 10); + } + sizeBuffer[sizeBuffer.length - 1] = 255; + return doneCallback(null, Buffer.concat([sizeBuffer, stringToBuffer(packet)])); + } + + sizeBuffer = Buffer.allocUnsafe(encodingLength.length + 2); + sizeBuffer[0] = 1; // is binary (true binary = 1) + for (var i = 0; i < encodingLength.length; i++) { + sizeBuffer[i + 1] = parseInt(encodingLength[i], 10); + } + sizeBuffer[sizeBuffer.length - 1] = 255; + + doneCallback(null, Buffer.concat([sizeBuffer, packet])); + } + + encodePacket(p, true, true, onBinaryPacketEncode); + +} + + +/* + * Decodes data when a payload is maybe expected. Strings are decoded by + * interpreting each byte as a key code for entries marked to start with 0. See + * description of encodePayloadAsBinary + + * @param {Buffer} data, callback method + * @api public + */ + +export function decodePayloadAsBinary (data, binaryType, callback) { + if (typeof binaryType === 'function') { + callback = binaryType; + binaryType = null; + } + + var bufferTail = data; + var buffers = []; + var i; + + while (bufferTail.length > 0) { + var strLen = ''; + var isString = bufferTail[0] === 0; + for (i = 1; ; i++) { + if (bufferTail[i] === 255) break; + // 310 = char length of Number.MAX_VALUE + if (strLen.length > 310) { + return callback(err, 0, 1); + } + strLen += '' + bufferTail[i]; + } + bufferTail = bufferTail.slice(strLen.length + 1); + + var msgLength = parseInt(strLen, 10); + + var msg = bufferTail.slice(1, msgLength + 1); + if (isString) msg = bufferToString(msg); + buffers.push(msg); + bufferTail = bufferTail.slice(msgLength + 1); + } + + var total = buffers.length; + for (i = 0; i < total; i++) { + var buffer = buffers[i]; + callback(decodePacket(buffer, binaryType, true), i, total); + } +}; diff --git a/packages/engine.io/lib/parser-v3/utf8.ts b/packages/engine.io/lib/parser-v3/utf8.ts new file mode 100644 index 00000000..b878740f --- /dev/null +++ b/packages/engine.io/lib/parser-v3/utf8.ts @@ -0,0 +1,210 @@ +/*! https://mths.be/utf8js v2.1.2 by @mathias */ + +var stringFromCharCode = String.fromCharCode; + +// Taken from https://mths.be/punycode +function ucs2decode(string) { + var output = []; + var counter = 0; + var length = string.length; + var value; + var extra; + while (counter < length) { + value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // high surrogate, and there is a next character + extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // low surrogate + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // unmatched surrogate; only append this code unit, in case the next + // code unit is the high surrogate of a surrogate pair + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; +} + +// Taken from https://mths.be/punycode +function ucs2encode(array) { + var length = array.length; + var index = -1; + var value; + var output = ''; + while (++index < length) { + value = array[index]; + if (value > 0xFFFF) { + value -= 0x10000; + output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); + value = 0xDC00 | value & 0x3FF; + } + output += stringFromCharCode(value); + } + return output; +} + +function checkScalarValue(codePoint, strict) { + if (codePoint >= 0xD800 && codePoint <= 0xDFFF) { + if (strict) { + throw Error( + 'Lone surrogate U+' + codePoint.toString(16).toUpperCase() + + ' is not a scalar value' + ); + } + return false; + } + return true; +} +/*--------------------------------------------------------------------------*/ + +function createByte(codePoint, shift) { + return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80); +} + +function encodeCodePoint(codePoint, strict) { + if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence + return stringFromCharCode(codePoint); + } + var symbol = ''; + if ((codePoint & 0xFFFFF800) == 0) { // 2-byte sequence + symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0); + } + else if ((codePoint & 0xFFFF0000) == 0) { // 3-byte sequence + if (!checkScalarValue(codePoint, strict)) { + codePoint = 0xFFFD; + } + symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0); + symbol += createByte(codePoint, 6); + } + else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence + symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0); + symbol += createByte(codePoint, 12); + symbol += createByte(codePoint, 6); + } + symbol += stringFromCharCode((codePoint & 0x3F) | 0x80); + return symbol; +} + +function utf8encode(string, opts) { + opts = opts || {}; + var strict = false !== opts.strict; + + var codePoints = ucs2decode(string); + var length = codePoints.length; + var index = -1; + var codePoint; + var byteString = ''; + while (++index < length) { + codePoint = codePoints[index]; + byteString += encodeCodePoint(codePoint, strict); + } + return byteString; +} + +/*--------------------------------------------------------------------------*/ + +function readContinuationByte() { + if (byteIndex >= byteCount) { + throw Error('Invalid byte index'); + } + + var continuationByte = byteArray[byteIndex] & 0xFF; + byteIndex++; + + if ((continuationByte & 0xC0) == 0x80) { + return continuationByte & 0x3F; + } + + // If we end up here, it’s not a continuation byte + throw Error('Invalid continuation byte'); +} + +function decodeSymbol(strict) { + var byte1; + var byte2; + var byte3; + var byte4; + var codePoint; + + if (byteIndex > byteCount) { + throw Error('Invalid byte index'); + } + + if (byteIndex == byteCount) { + return false; + } + + // Read first byte + byte1 = byteArray[byteIndex] & 0xFF; + byteIndex++; + + // 1-byte sequence (no continuation bytes) + if ((byte1 & 0x80) == 0) { + return byte1; + } + + // 2-byte sequence + if ((byte1 & 0xE0) == 0xC0) { + byte2 = readContinuationByte(); + codePoint = ((byte1 & 0x1F) << 6) | byte2; + if (codePoint >= 0x80) { + return codePoint; + } else { + throw Error('Invalid continuation byte'); + } + } + + // 3-byte sequence (may include unpaired surrogates) + if ((byte1 & 0xF0) == 0xE0) { + byte2 = readContinuationByte(); + byte3 = readContinuationByte(); + codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3; + if (codePoint >= 0x0800) { + return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD; + } else { + throw Error('Invalid continuation byte'); + } + } + + // 4-byte sequence + if ((byte1 & 0xF8) == 0xF0) { + byte2 = readContinuationByte(); + byte3 = readContinuationByte(); + byte4 = readContinuationByte(); + codePoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0C) | + (byte3 << 0x06) | byte4; + if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) { + return codePoint; + } + } + + throw Error('Invalid UTF-8 detected'); +} + +var byteArray; +var byteCount; +var byteIndex; +function utf8decode(byteString, opts) { + opts = opts || {}; + var strict = false !== opts.strict; + + byteArray = ucs2decode(byteString); + byteCount = byteArray.length; + byteIndex = 0; + var codePoints = []; + var tmp; + while ((tmp = decodeSymbol(strict)) !== false) { + codePoints.push(tmp); + } + return ucs2encode(codePoints); +} + +module.exports = { + version: '2.1.2', + encode: utf8encode, + decode: utf8decode +}; diff --git a/packages/engine.io/lib/server.ts b/packages/engine.io/lib/server.ts new file mode 100644 index 00000000..a010092f --- /dev/null +++ b/packages/engine.io/lib/server.ts @@ -0,0 +1,1056 @@ +import * as qs from "querystring"; +import { parse } from "url"; +import * as base64id from "base64id"; +import transports from "./transports"; +import { EventEmitter } from "events"; +import { Socket } from "./socket"; +import debugModule from "debug"; +import { serialize } from "cookie"; +import { Server as DEFAULT_WS_ENGINE } from "ws"; +import type { + IncomingMessage, + Server as HttpServer, + ServerResponse, +} from "http"; +import type { CookieSerializeOptions } from "cookie"; +import type { CorsOptions, CorsOptionsDelegate } from "cors"; +import type { Duplex } from "stream"; +import { WebTransport } from "./transports/webtransport"; +import { createPacketDecoderStream } from "engine.io-parser"; +import type { EngineRequest } from "./transport"; + +const debug = debugModule("engine"); + +const kResponseHeaders = Symbol("responseHeaders"); + +type Transport = "polling" | "websocket" | "webtransport"; + +export interface AttachOptions { + /** + * name of the path to capture + * @default "/engine.io" + */ + path?: string; + /** + * destroy unhandled upgrade requests + * @default true + */ + destroyUpgrade?: boolean; + /** + * milliseconds after which unhandled requests are ended + * @default 1000 + */ + destroyUpgradeTimeout?: number; + + /** + * Whether we should add a trailing slash to the request path. + * @default true + */ + addTrailingSlash?: boolean; +} + +export interface ServerOptions { + /** + * how many ms without a pong packet to consider the connection closed + * @default 20000 + */ + pingTimeout?: number; + /** + * how many ms before sending a new ping packet + * @default 25000 + */ + pingInterval?: number; + /** + * how many ms before an uncompleted transport upgrade is cancelled + * @default 10000 + */ + upgradeTimeout?: number; + /** + * how many bytes or characters a message can be, before closing the session (to avoid DoS). + * @default 1e5 (100 KB) + */ + maxHttpBufferSize?: number; + /** + * A function that receives a given handshake or upgrade request as its first parameter, + * and can decide whether to continue or not. The second argument is a function that needs + * to be called with the decided information: fn(err, success), where success is a boolean + * value where false means that the request is rejected, and err is an error code. + */ + allowRequest?: ( + req: IncomingMessage, + fn: (err: string | null | undefined, success: boolean) => void + ) => void; + /** + * The low-level transports that are enabled. WebTransport is disabled by default and must be manually enabled: + * + * @example + * new Server({ + * transports: ["polling", "websocket", "webtransport"] + * }); + * + * @default ["polling", "websocket"] + */ + transports?: Transport[]; + /** + * whether to allow transport upgrades + * @default true + */ + allowUpgrades?: boolean; + /** + * parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable. + * @default false + */ + perMessageDeflate?: boolean | object; + /** + * parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable. + * @default true + */ + httpCompression?: boolean | object; + /** + * what WebSocket server implementation to use. Specified module must + * conform to the ws interface (see ws module api docs). + * An alternative c++ addon is also available by installing eiows module. + * + * @default `require("ws").Server` + */ + wsEngine?: any; + /** + * an optional packet which will be concatenated to the handshake packet emitted by Engine.IO. + */ + initialPacket?: any; + /** + * configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie + * might be used for sticky-session. Defaults to not sending any cookie. + * @default false + */ + cookie?: (CookieSerializeOptions & { name: string }) | boolean; + /** + * the options that will be forwarded to the cors module + */ + cors?: CorsOptions | CorsOptionsDelegate; + /** + * whether to enable compatibility with Socket.IO v2 clients + * @default false + */ + allowEIO3?: boolean; +} + +/** + * An Express-compatible middleware. + * + * Middleware functions are functions that have access to the request object (req), the response object (res), and the + * next middleware function in the application’s request-response cycle. + * + * @see https://expressjs.com/en/guide/using-middleware.html + */ +type Middleware = ( + req: IncomingMessage, + res: ServerResponse, + next: (err?: any) => void +) => void; + +function parseSessionId(data: string) { + try { + const parsed = JSON.parse(data); + if (typeof parsed.sid === "string") { + return parsed.sid; + } + } catch (e) {} +} + +export abstract class BaseServer extends EventEmitter { + public opts: ServerOptions; + + // TODO for the next major release: use a Map instead + protected clients: Record; + public clientsCount: number; + protected middlewares: Middleware[] = []; + + /** + * Server constructor. + * + * @param {Object} opts - options + */ + constructor(opts: ServerOptions = {}) { + super(); + + this.clients = {}; + this.clientsCount = 0; + + this.opts = Object.assign( + { + wsEngine: DEFAULT_WS_ENGINE, + pingTimeout: 20000, + pingInterval: 25000, + upgradeTimeout: 10000, + maxHttpBufferSize: 1e6, + transports: ["polling", "websocket"], // WebTransport is disabled by default + allowUpgrades: true, + httpCompression: { + threshold: 1024, + }, + cors: false, + allowEIO3: false, + }, + opts + ); + + if (opts.cookie) { + this.opts.cookie = Object.assign( + { + name: "io", + path: "/", + // @ts-ignore + httpOnly: opts.cookie.path !== false, + sameSite: "lax", + }, + opts.cookie + ); + } + + if (this.opts.cors) { + this.use(require("cors")(this.opts.cors)); + } + + if (opts.perMessageDeflate) { + this.opts.perMessageDeflate = Object.assign( + { + threshold: 1024, + }, + opts.perMessageDeflate + ); + } + + this.init(); + } + + protected abstract init(); + + /** + * Compute the pathname of the requests that are handled by the server + * @param options + * @protected + */ + protected _computePath(options: AttachOptions) { + let path = (options.path || "/engine.io").replace(/\/$/, ""); + + if (options.addTrailingSlash !== false) { + // normalize path + path += "/"; + } + + return path; + } + + /** + * Returns a list of available transports for upgrade given a certain transport. + * + * @return {Array} + */ + public upgrades(transport: string) { + if (!this.opts.allowUpgrades) return []; + return transports[transport].upgradesTo || []; + } + + /** + * Verifies a request. + * + * @param {EngineRequest} req + * @param upgrade - whether it's an upgrade request + * @param fn + * @protected + */ + protected verify( + req: any, + upgrade: boolean, + fn: (errorCode?: number, errorContext?: any) => void + ) { + // transport check + const transport = req._query.transport; + // WebTransport does not go through the verify() method, see the onWebTransportSession() method + if ( + !~this.opts.transports.indexOf(transport) || + transport === "webtransport" + ) { + debug('unknown transport "%s"', transport); + return fn(Server.errors.UNKNOWN_TRANSPORT, { transport }); + } + + // 'Origin' header check + const isOriginInvalid = checkInvalidHeaderChar(req.headers.origin); + if (isOriginInvalid) { + const origin = req.headers.origin; + req.headers.origin = null; + debug("origin header invalid"); + return fn(Server.errors.BAD_REQUEST, { + name: "INVALID_ORIGIN", + origin, + }); + } + + // sid check + const sid = req._query.sid; + if (sid) { + if (!this.clients.hasOwnProperty(sid)) { + debug('unknown sid "%s"', sid); + return fn(Server.errors.UNKNOWN_SID, { + sid, + }); + } + const previousTransport = this.clients[sid].transport.name; + if (!upgrade && previousTransport !== transport) { + debug("bad request: unexpected transport without upgrade"); + return fn(Server.errors.BAD_REQUEST, { + name: "TRANSPORT_MISMATCH", + transport, + previousTransport, + }); + } + } else { + // handshake is GET only + if ("GET" !== req.method) { + return fn(Server.errors.BAD_HANDSHAKE_METHOD, { + method: req.method, + }); + } + + if (transport === "websocket" && !upgrade) { + debug("invalid transport upgrade"); + return fn(Server.errors.BAD_REQUEST, { + name: "TRANSPORT_HANDSHAKE_ERROR", + }); + } + + if (!this.opts.allowRequest) return fn(); + + return this.opts.allowRequest(req, (message, success) => { + if (!success) { + return fn(Server.errors.FORBIDDEN, { + message, + }); + } + fn(); + }); + } + + fn(); + } + + /** + * Adds a new middleware. + * + * @example + * import helmet from "helmet"; + * + * engine.use(helmet()); + * + * @param fn + */ + public use(fn: any) { + this.middlewares.push(fn); + } + + /** + * Apply the middlewares to the request. + * + * @param req + * @param res + * @param callback + * @protected + */ + protected _applyMiddlewares( + req: IncomingMessage, + res: ServerResponse, + callback: (err?: any) => void + ) { + if (this.middlewares.length === 0) { + debug("no middleware to apply, skipping"); + return callback(); + } + + const apply = (i) => { + debug("applying middleware n°%d", i + 1); + this.middlewares[i](req, res, (err?: any) => { + if (err) { + return callback(err); + } + + if (i + 1 < this.middlewares.length) { + apply(i + 1); + } else { + callback(); + } + }); + }; + + apply(0); + } + + /** + * Closes all clients. + */ + public close() { + debug("closing all open clients"); + for (let i in this.clients) { + if (this.clients.hasOwnProperty(i)) { + this.clients[i].close(true); + } + } + this.cleanup(); + return this; + } + + protected abstract cleanup(); + + /** + * generate a socket id. + * Overwrite this method to generate your custom socket id + * + * @param {IncomingMessage} req - the request object + */ + public generateId(req: IncomingMessage) { + return base64id.generateId(); + } + + /** + * Handshakes a new client. + * + * @param {String} transportName + * @param {Object} req - the request object + * @param {Function} closeConnection + * + * @protected + */ + protected async handshake( + transportName: string, + req: any, + closeConnection: (errorCode?: number, errorContext?: any) => void + ) { + const protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default + if (protocol === 3 && !this.opts.allowEIO3) { + debug("unsupported protocol version"); + this.emit("connection_error", { + req, + code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION, + message: + Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION], + context: { + protocol, + }, + }); + closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION); + return; + } + + let id; + try { + id = await this.generateId(req); + } catch (e) { + debug("error while generating an id"); + this.emit("connection_error", { + req, + code: Server.errors.BAD_REQUEST, + message: Server.errorMessages[Server.errors.BAD_REQUEST], + context: { + name: "ID_GENERATION_ERROR", + error: e, + }, + }); + closeConnection(Server.errors.BAD_REQUEST); + return; + } + + debug('handshaking client "%s"', id); + + try { + var transport = this.createTransport(transportName, req); + if ("polling" === transportName) { + transport.maxHttpBufferSize = this.opts.maxHttpBufferSize; + transport.httpCompression = this.opts.httpCompression; + } else if ("websocket" === transportName) { + transport.perMessageDeflate = this.opts.perMessageDeflate; + } + } catch (e) { + debug('error handshaking to transport "%s"', transportName); + this.emit("connection_error", { + req, + code: Server.errors.BAD_REQUEST, + message: Server.errorMessages[Server.errors.BAD_REQUEST], + context: { + name: "TRANSPORT_HANDSHAKE_ERROR", + error: e, + }, + }); + closeConnection(Server.errors.BAD_REQUEST); + return; + } + const socket = new Socket(id, this, transport, req, protocol); + + transport.on("headers", (headers, req) => { + const isInitialRequest = !req._query.sid; + + if (isInitialRequest) { + if (this.opts.cookie) { + headers["Set-Cookie"] = [ + // @ts-ignore + serialize(this.opts.cookie.name, id, this.opts.cookie), + ]; + } + this.emit("initial_headers", headers, req); + } + this.emit("headers", headers, req); + }); + + transport.onRequest(req); + + this.clients[id] = socket; + this.clientsCount++; + + socket.once("close", () => { + delete this.clients[id]; + this.clientsCount--; + }); + + this.emit("connection", socket); + + return transport; + } + + public async onWebTransportSession(session: any) { + const timeout = setTimeout(() => { + debug( + "the client failed to establish a bidirectional stream in the given period" + ); + session.close(); + }, this.opts.upgradeTimeout); + + const streamReader = session.incomingBidirectionalStreams.getReader(); + const result = await streamReader.read(); + + if (result.done) { + debug("session is closed"); + return; + } + + const stream = result.value; + const transformStream = createPacketDecoderStream( + this.opts.maxHttpBufferSize, + "nodebuffer" + ); + const reader = stream.readable.pipeThrough(transformStream).getReader(); + + // reading the first packet of the stream + const { value, done } = await reader.read(); + if (done) { + debug("stream is closed"); + return; + } + + clearTimeout(timeout); + + if (value.type !== "open") { + debug("invalid WebTransport handshake"); + return session.close(); + } + + if (value.data === undefined) { + const transport = new WebTransport(session, stream, reader); + + // note: we cannot use "this.generateId()", because there is no "req" argument + const id = base64id.generateId(); + debug('handshaking client "%s" (WebTransport)', id); + + const socket = new Socket(id, this, transport, null, 4); + + this.clients[id] = socket; + this.clientsCount++; + + socket.once("close", () => { + delete this.clients[id]; + this.clientsCount--; + }); + + this.emit("connection", socket); + return; + } + + const sid = parseSessionId(value.data); + + if (!sid) { + debug("invalid WebTransport handshake"); + return session.close(); + } + + const client = this.clients[sid]; + + if (!client) { + debug("upgrade attempt for closed client"); + session.close(); + } else if (client.upgrading) { + debug("transport has already been trying to upgrade"); + session.close(); + } else if (client.upgraded) { + debug("transport had already been upgraded"); + session.close(); + } else { + debug("upgrading existing transport"); + + const transport = new WebTransport(session, stream, reader); + client._maybeUpgrade(transport); + } + } + + protected abstract createTransport(transportName, req); + + /** + * Protocol errors mappings. + */ + + static errors = { + UNKNOWN_TRANSPORT: 0, + UNKNOWN_SID: 1, + BAD_HANDSHAKE_METHOD: 2, + BAD_REQUEST: 3, + FORBIDDEN: 4, + UNSUPPORTED_PROTOCOL_VERSION: 5, + }; + + static errorMessages = { + 0: "Transport unknown", + 1: "Session ID unknown", + 2: "Bad handshake method", + 3: "Bad request", + 4: "Forbidden", + 5: "Unsupported protocol version", + }; +} + +/** + * Exposes a subset of the http.ServerResponse interface, in order to be able to apply the middlewares to an upgrade + * request. + * + * @see https://nodejs.org/api/http.html#class-httpserverresponse + */ +class WebSocketResponse { + constructor(readonly req, readonly socket: Duplex) { + // temporarily store the response headers on the req object (see the "headers" event) + req[kResponseHeaders] = {}; + } + + public setHeader(name: string, value: any) { + this.req[kResponseHeaders][name] = value; + } + + public getHeader(name: string) { + return this.req[kResponseHeaders][name]; + } + + public removeHeader(name: string) { + delete this.req[kResponseHeaders][name]; + } + + public write() {} + + public writeHead() {} + + public end() { + // we could return a proper error code, but the WebSocket client will emit an "error" event anyway. + this.socket.destroy(); + } +} + +export class Server extends BaseServer { + public httpServer?: HttpServer; + private ws: any; + + /** + * Initialize websocket server + * + * @protected + */ + protected init() { + if (!~this.opts.transports.indexOf("websocket")) return; + + if (this.ws) this.ws.close(); + + this.ws = new this.opts.wsEngine({ + noServer: true, + clientTracking: false, + perMessageDeflate: this.opts.perMessageDeflate, + maxPayload: this.opts.maxHttpBufferSize, + }); + + if (typeof this.ws.on === "function") { + this.ws.on("headers", (headersArray, req) => { + // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats) + // we could also try to parse the array and then sync the values, but that will be error-prone + const additionalHeaders = req[kResponseHeaders] || {}; + delete req[kResponseHeaders]; + + const isInitialRequest = !req._query.sid; + if (isInitialRequest) { + this.emit("initial_headers", additionalHeaders, req); + } + + this.emit("headers", additionalHeaders, req); + + debug("writing headers: %j", additionalHeaders); + Object.keys(additionalHeaders).forEach((key) => { + headersArray.push(`${key}: ${additionalHeaders[key]}`); + }); + }); + } + } + + protected cleanup() { + if (this.ws) { + debug("closing webSocketServer"); + this.ws.close(); + // don't delete this.ws because it can be used again if the http server starts listening again + } + } + + /** + * Prepares a request by processing the query string. + * + * @private + */ + private prepare(req: EngineRequest) { + // try to leverage pre-existing `req._query` (e.g: from connect) + if (!req._query) { + req._query = ( + ~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {} + ) as Record; + } + } + + protected createTransport(transportName: string, req: IncomingMessage) { + return new transports[transportName](req); + } + + /** + * Handles an Engine.IO HTTP request. + * + * @param {EngineRequest} req + * @param {ServerResponse} res + */ + public handleRequest(req: EngineRequest, res: ServerResponse) { + debug('handling "%s" http request "%s"', req.method, req.url); + this.prepare(req); + req.res = res; + + const callback = (errorCode, errorContext) => { + if (errorCode !== undefined) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + abortRequest(res, errorCode, errorContext); + return; + } + + if (req._query.sid) { + debug("setting new request for existing client"); + this.clients[req._query.sid].transport.onRequest(req); + } else { + const closeConnection = (errorCode, errorContext) => + abortRequest(res, errorCode, errorContext); + this.handshake(req._query.transport, req, closeConnection); + } + }; + + this._applyMiddlewares(req, res, (err) => { + if (err) { + callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" }); + } else { + this.verify(req, false, callback); + } + }); + } + + /** + * Handles an Engine.IO HTTP Upgrade. + */ + public handleUpgrade( + req: EngineRequest, + socket: Duplex, + upgradeHead: Buffer + ) { + this.prepare(req); + + const res = new WebSocketResponse(req, socket); + const callback = (errorCode, errorContext) => { + if (errorCode !== undefined) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + abortUpgrade(socket, errorCode, errorContext); + return; + } + + const head = Buffer.from(upgradeHead); + upgradeHead = null; + + // some middlewares (like express-session) wait for the writeHead() call to flush their headers + // see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 + res.writeHead(); + + // delegate to ws + this.ws.handleUpgrade(req, socket, head, (websocket) => { + this.onWebSocket(req, socket, websocket); + }); + }; + + this._applyMiddlewares(req, res as unknown as ServerResponse, (err) => { + if (err) { + callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" }); + } else { + this.verify(req, true, callback); + } + }); + } + + /** + * Called upon a ws.io connection. + * + * @param {ws.Socket} websocket + * @private + */ + private onWebSocket(req, socket, websocket) { + websocket.on("error", onUpgradeError); + + if ( + transports[req._query.transport] !== undefined && + !transports[req._query.transport].prototype.handlesUpgrades + ) { + debug("transport doesnt handle upgraded requests"); + websocket.close(); + return; + } + + // get client id + const id = req._query.sid; + + // keep a reference to the ws.Socket + req.websocket = websocket; + + if (id) { + const client = this.clients[id]; + if (!client) { + debug("upgrade attempt for closed client"); + websocket.close(); + } else if (client.upgrading) { + debug("transport has already been trying to upgrade"); + websocket.close(); + } else if (client.upgraded) { + debug("transport had already been upgraded"); + websocket.close(); + } else { + debug("upgrading existing transport"); + + // transport error handling takes over + websocket.removeListener("error", onUpgradeError); + + const transport = this.createTransport(req._query.transport, req); + transport.perMessageDeflate = this.opts.perMessageDeflate; + client._maybeUpgrade(transport); + } + } else { + const closeConnection = (errorCode, errorContext) => + abortUpgrade(socket, errorCode, errorContext); + this.handshake(req._query.transport, req, closeConnection); + } + + function onUpgradeError() { + debug("websocket error before upgrade"); + // websocket.close() not needed + } + } + + /** + * Captures upgrade requests for a http.Server. + * + * @param {http.Server} server + * @param {Object} options + */ + public attach(server: HttpServer, options: AttachOptions = {}) { + const path = this._computePath(options); + const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000; + + function check(req) { + // TODO use `path === new URL(...).pathname` in the next major release (ref: https://nodejs.org/api/url.html) + return path === req.url.slice(0, path.length); + } + + // cache and clean up listeners + const listeners = server.listeners("request").slice(0); + server.removeAllListeners("request"); + server.on("close", this.close.bind(this)); + server.on("listening", this.init.bind(this)); + + // add request handler + server.on("request", (req, res) => { + if (check(req)) { + debug('intercepting request for path "%s"', path); + this.handleRequest(req as EngineRequest, res); + } else { + let i = 0; + const l = listeners.length; + for (; i < l; i++) { + listeners[i].call(server, req, res); + } + } + }); + + if (~this.opts.transports.indexOf("websocket")) { + server.on("upgrade", (req, socket, head) => { + if (check(req)) { + this.handleUpgrade(req as EngineRequest, socket, head); + } else if (false !== options.destroyUpgrade) { + // default node behavior is to disconnect when no handlers + // but by adding a handler, we prevent that + // and if no eio thing handles the upgrade + // then the socket needs to die! + setTimeout(function () { + // @ts-ignore + if (socket.writable && socket.bytesWritten <= 0) { + socket.on("error", (e) => { + debug("error while destroying upgrade: %s", e.message); + }); + return socket.end(); + } + }, destroyUpgradeTimeout); + } + }); + } + } +} + +/** + * Close the HTTP long-polling request + * + * @param res - the response object + * @param errorCode - the error code + * @param errorContext - additional error context + * + * @private + */ + +function abortRequest(res, errorCode, errorContext) { + const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400; + const message = + errorContext && errorContext.message + ? errorContext.message + : Server.errorMessages[errorCode]; + + res.writeHead(statusCode, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + code: errorCode, + message, + }) + ); +} + +/** + * Close the WebSocket connection + * + * @param {net.Socket} socket + * @param {string} errorCode - the error code + * @param {object} errorContext - additional error context + */ + +function abortUpgrade( + socket, + errorCode, + errorContext: { message?: string } = {} +) { + socket.on("error", () => { + debug("ignoring error from closed connection"); + }); + if (socket.writable) { + const message = errorContext.message || Server.errorMessages[errorCode]; + const length = Buffer.byteLength(message); + socket.write( + "HTTP/1.1 400 Bad Request\r\n" + + "Connection: close\r\n" + + "Content-type: text/html\r\n" + + "Content-Length: " + + length + + "\r\n" + + "\r\n" + + message + ); + } + socket.destroy(); +} + +/* eslint-disable */ + +/** + * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354 + * + * True if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * + * checkInvalidHeaderChar() is currently designed to be inlinable by v8, + * so take care when making changes to the implementation so that the source + * code size does not exceed v8's default max_inlined_source_size setting. + **/ +// prettier-ignore +const validHdrChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ... + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255 +] + +function checkInvalidHeaderChar(val) { + val += ""; + if (val.length < 1) return false; + if (!validHdrChars[val.charCodeAt(0)]) { + debug('invalid header, index 0, char "%s"', val.charCodeAt(0)); + return true; + } + if (val.length < 2) return false; + if (!validHdrChars[val.charCodeAt(1)]) { + debug('invalid header, index 1, char "%s"', val.charCodeAt(1)); + return true; + } + if (val.length < 3) return false; + if (!validHdrChars[val.charCodeAt(2)]) { + debug('invalid header, index 2, char "%s"', val.charCodeAt(2)); + return true; + } + if (val.length < 4) return false; + if (!validHdrChars[val.charCodeAt(3)]) { + debug('invalid header, index 3, char "%s"', val.charCodeAt(3)); + return true; + } + for (let i = 4; i < val.length; ++i) { + if (!validHdrChars[val.charCodeAt(i)]) { + debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i)); + return true; + } + } + return false; +} diff --git a/packages/engine.io/lib/socket.ts b/packages/engine.io/lib/socket.ts new file mode 100644 index 00000000..d9f58a68 --- /dev/null +++ b/packages/engine.io/lib/socket.ts @@ -0,0 +1,588 @@ +import { EventEmitter } from "events"; +import debugModule from "debug"; +import type { IncomingMessage } from "http"; +import type { EngineRequest, Transport } from "./transport"; +import type { BaseServer } from "./server"; +import { setTimeout, clearTimeout } from "timers"; +import type { Packet, PacketType, RawData } from "engine.io-parser"; + +const debug = debugModule("engine:socket"); + +export interface SendOptions { + compress?: boolean; +} + +type ReadyState = "opening" | "open" | "closing" | "closed"; + +type SendCallback = (transport: Transport) => void; + +export class Socket extends EventEmitter { + /** + * The revision of the protocol: + * + * - 3rd is used in Engine.IO v3 / Socket.IO v2 + * - 4th is used in Engine.IO v4 and above / Socket.IO v3 and above + * + * It is found in the `EIO` query parameters of the HTTP requests. + * + * @see https://github.com/socketio/engine.io-protocol + */ + public readonly protocol: number; + /** + * A reference to the first HTTP request of the session + * + * TODO for the next major release: remove it + */ + public request: IncomingMessage; + /** + * The IP address of the client. + */ + public readonly remoteAddress: string; + + /** + * The current state of the socket. + */ + public _readyState: ReadyState = "opening"; + /** + * The current low-level transport. + */ + public transport: Transport; + + private server: BaseServer; + /* private */ upgrading = false; + /* private */ upgraded = false; + private writeBuffer: Packet[] = []; + private packetsFn: SendCallback[] = []; + private sentCallbackFn: SendCallback[][] = []; + private cleanupFn: any[] = []; + private pingTimeoutTimer; + private pingIntervalTimer; + + /** + * This is the session identifier that the client will use in the subsequent HTTP requests. It must not be shared with + * others parties, as it might lead to session hijacking. + * + * @private + */ + private readonly id: string; + + get readyState() { + return this._readyState; + } + + set readyState(state: ReadyState) { + debug("readyState updated from %s to %s", this._readyState, state); + this._readyState = state; + } + + constructor( + id: string, + server: BaseServer, + transport: Transport, + req: EngineRequest, + protocol: number + ) { + super(); + this.id = id; + this.server = server; + this.request = req; + this.protocol = protocol; + + // Cache IP since it might not be in the req later + if (req) { + if (req.websocket && req.websocket._socket) { + this.remoteAddress = req.websocket._socket.remoteAddress; + } else { + this.remoteAddress = req.connection.remoteAddress; + } + } else { + // TODO there is currently no way to get the IP address of the client when it connects with WebTransport + // see https://github.com/fails-components/webtransport/issues/114 + } + + this.pingTimeoutTimer = null; + this.pingIntervalTimer = null; + + this.setTransport(transport); + this.onOpen(); + } + + /** + * Called upon transport considered open. + * + * @private + */ + private onOpen() { + this.readyState = "open"; + + // sends an `open` packet + this.transport.sid = this.id; + this.sendPacket( + "open", + JSON.stringify({ + sid: this.id, + upgrades: this.getAvailableUpgrades(), + pingInterval: this.server.opts.pingInterval, + pingTimeout: this.server.opts.pingTimeout, + maxPayload: this.server.opts.maxHttpBufferSize, + }) + ); + + if (this.server.opts.initialPacket) { + this.sendPacket("message", this.server.opts.initialPacket); + } + + this.emit("open"); + + if (this.protocol === 3) { + // in protocol v3, the client sends a ping, and the server answers with a pong + this.resetPingTimeout(); + } else { + // in protocol v4, the server sends a ping, and the client answers with a pong + this.schedulePing(); + } + } + + /** + * Called upon transport packet. + * + * @param {Object} packet + * @private + */ + private onPacket(packet: Packet) { + if ("open" !== this.readyState) { + return debug("packet received with closed socket"); + } + // export packet event + debug(`received packet ${packet.type}`); + this.emit("packet", packet); + + switch (packet.type) { + case "ping": + if (this.transport.protocol !== 3) { + this.onError(new Error("invalid heartbeat direction")); + return; + } + debug("got ping"); + this.pingTimeoutTimer.refresh(); + this.sendPacket("pong"); + this.emit("heartbeat"); + break; + + case "pong": + if (this.transport.protocol === 3) { + this.onError(new Error("invalid heartbeat direction")); + return; + } + debug("got pong"); + clearTimeout(this.pingTimeoutTimer); + this.pingIntervalTimer.refresh(); + this.emit("heartbeat"); + break; + + case "error": + this.onClose("parse error"); + break; + + case "message": + this.emit("data", packet.data); + this.emit("message", packet.data); + break; + } + } + + /** + * Called upon transport error. + * + * @param {Error} err - error object + * @private + */ + private onError(err: Error) { + debug("transport error"); + this.onClose("transport error", err); + } + + /** + * Pings client every `this.pingInterval` and expects response + * within `this.pingTimeout` or closes connection. + * + * @private + */ + private schedulePing() { + this.pingIntervalTimer = setTimeout(() => { + debug( + "writing ping packet - expecting pong within %sms", + this.server.opts.pingTimeout + ); + this.sendPacket("ping"); + this.resetPingTimeout(); + }, this.server.opts.pingInterval); + } + + /** + * Resets ping timeout. + * + * @private + */ + private resetPingTimeout() { + clearTimeout(this.pingTimeoutTimer); + this.pingTimeoutTimer = setTimeout( + () => { + if (this.readyState === "closed") return; + this.onClose("ping timeout"); + }, + this.protocol === 3 + ? this.server.opts.pingInterval + this.server.opts.pingTimeout + : this.server.opts.pingTimeout + ); + } + + /** + * Attaches handlers for the given transport. + * + * @param {Transport} transport + * @private + */ + private setTransport(transport: Transport) { + const onError = this.onError.bind(this); + const onReady = () => this.flush(); + const onPacket = this.onPacket.bind(this); + const onDrain = this.onDrain.bind(this); + const onClose = this.onClose.bind(this, "transport close"); + + this.transport = transport; + this.transport.once("error", onError); + this.transport.on("ready", onReady); + this.transport.on("packet", onPacket); + this.transport.on("drain", onDrain); + this.transport.once("close", onClose); + + this.cleanupFn.push(function () { + transport.removeListener("error", onError); + transport.removeListener("ready", onReady); + transport.removeListener("packet", onPacket); + transport.removeListener("drain", onDrain); + transport.removeListener("close", onClose); + }); + } + + /** + * Upon transport "drain" event + * + * @private + */ + private onDrain() { + if (this.sentCallbackFn.length > 0) { + debug("executing batch send callback"); + const seqFn = this.sentCallbackFn.shift(); + if (seqFn) { + for (let i = 0; i < seqFn.length; i++) { + seqFn[i](this.transport); + } + } + } + } + + /** + * Upgrades socket to the given transport + * + * @param {Transport} transport + * @private + */ + /* private */ _maybeUpgrade(transport: Transport) { + debug( + 'might upgrade socket transport from "%s" to "%s"', + this.transport.name, + transport.name + ); + + this.upgrading = true; + + // set transport upgrade timer + const upgradeTimeoutTimer = setTimeout(() => { + debug("client did not complete upgrade - closing transport"); + cleanup(); + if ("open" === transport.readyState) { + transport.close(); + } + }, this.server.opts.upgradeTimeout); + + let checkIntervalTimer; + + const onPacket = (packet) => { + if ("ping" === packet.type && "probe" === packet.data) { + debug("got probe ping packet, sending pong"); + transport.send([{ type: "pong", data: "probe" }]); + this.emit("upgrading", transport); + clearInterval(checkIntervalTimer); + checkIntervalTimer = setInterval(check, 100); + } else if ("upgrade" === packet.type && this.readyState !== "closed") { + debug("got upgrade packet - upgrading"); + cleanup(); + this.transport.discard(); + this.upgraded = true; + this.clearTransport(); + this.setTransport(transport); + this.emit("upgrade", transport); + this.flush(); + if (this.readyState === "closing") { + transport.close(() => { + this.onClose("forced close"); + }); + } + } else { + cleanup(); + transport.close(); + } + }; + + // we force a polling cycle to ensure a fast upgrade + const check = () => { + if ("polling" === this.transport.name && this.transport.writable) { + debug("writing a noop packet to polling for fast upgrade"); + this.transport.send([{ type: "noop" }]); + } + }; + + const cleanup = () => { + this.upgrading = false; + + clearInterval(checkIntervalTimer); + clearTimeout(upgradeTimeoutTimer); + + transport.removeListener("packet", onPacket); + transport.removeListener("close", onTransportClose); + transport.removeListener("error", onError); + this.removeListener("close", onClose); + }; + + const onError = (err) => { + debug("client did not complete upgrade - %s", err); + cleanup(); + transport.close(); + transport = null; + }; + + const onTransportClose = () => { + onError("transport closed"); + }; + + const onClose = () => { + onError("socket closed"); + }; + + transport.on("packet", onPacket); + transport.once("close", onTransportClose); + transport.once("error", onError); + + this.once("close", onClose); + } + + /** + * Clears listeners and timers associated with current transport. + * + * @private + */ + private clearTransport() { + let cleanup; + + const toCleanUp = this.cleanupFn.length; + + for (let i = 0; i < toCleanUp; i++) { + cleanup = this.cleanupFn.shift(); + cleanup(); + } + + // silence further transport errors and prevent uncaught exceptions + this.transport.on("error", function () { + debug("error triggered by discarded transport"); + }); + + // ensure transport won't stay open + this.transport.close(); + + clearTimeout(this.pingTimeoutTimer); + } + + /** + * Called upon transport considered closed. + * Possible reasons: `ping timeout`, `client error`, `parse error`, + * `transport error`, `server close`, `transport close` + */ + private onClose(reason: string, description?) { + if ("closed" !== this.readyState) { + this.readyState = "closed"; + + // clear timers + clearTimeout(this.pingIntervalTimer); + clearTimeout(this.pingTimeoutTimer); + + // clean writeBuffer in next tick, so developers can still + // grab the writeBuffer on 'close' event + process.nextTick(() => { + this.writeBuffer = []; + }); + this.packetsFn = []; + this.sentCallbackFn = []; + this.clearTransport(); + this.emit("close", reason, description); + } + } + + /** + * Sends a message packet. + * + * @param {Object} data + * @param {Object} options + * @param {Function} callback + * @return {Socket} for chaining + */ + public send(data: RawData, options?: SendOptions, callback?: SendCallback) { + this.sendPacket("message", data, options, callback); + return this; + } + + /** + * Alias of {@link send}. + * + * @param data + * @param options + * @param callback + */ + public write(data: RawData, options?: SendOptions, callback?: SendCallback) { + this.sendPacket("message", data, options, callback); + return this; + } + + /** + * Sends a packet. + * + * @param {String} type - packet type + * @param {String} data + * @param {Object} options + * @param {Function} callback + * + * @private + */ + private sendPacket( + type: PacketType, + data?: RawData, + options: SendOptions = {}, + callback?: SendCallback + ) { + if ("function" === typeof options) { + callback = options; + options = {}; + } + + if ("closing" !== this.readyState && "closed" !== this.readyState) { + debug('sending packet "%s" (%s)', type, data); + + // compression is enabled by default + options.compress = options.compress !== false; + + const packet: Packet = { + type, + options: options as { compress: boolean }, + }; + + if (data) packet.data = data; + + // exports packetCreate event + this.emit("packetCreate", packet); + + this.writeBuffer.push(packet); + + // add send callback to object, if defined + if ("function" === typeof callback) this.packetsFn.push(callback); + + this.flush(); + } + } + + /** + * Attempts to flush the packets buffer. + * + * @private + */ + private flush() { + if ( + "closed" !== this.readyState && + this.transport.writable && + this.writeBuffer.length + ) { + debug("flushing buffer to transport"); + this.emit("flush", this.writeBuffer); + this.server.emit("flush", this, this.writeBuffer); + const wbuf = this.writeBuffer; + this.writeBuffer = []; + + if (this.packetsFn.length) { + this.sentCallbackFn.push(this.packetsFn); + this.packetsFn = []; + } else { + this.sentCallbackFn.push(null); + } + + this.transport.send(wbuf); + this.emit("drain"); + this.server.emit("drain", this); + } + } + + /** + * Get available upgrades for this socket. + * + * @private + */ + private getAvailableUpgrades() { + const availableUpgrades = []; + const allUpgrades = this.server.upgrades(this.transport.name); + for (let i = 0; i < allUpgrades.length; ++i) { + const upg = allUpgrades[i]; + if (this.server.opts.transports.indexOf(upg) !== -1) { + availableUpgrades.push(upg); + } + } + return availableUpgrades; + } + + /** + * Closes the socket and underlying transport. + * + * @param {Boolean} discard - optional, discard the transport + * @return {Socket} for chaining + */ + public close(discard?: boolean) { + if ("open" !== this.readyState) return; + + this.readyState = "closing"; + + if (this.writeBuffer.length) { + debug( + "there are %d remaining packets in the buffer, waiting for the 'drain' event", + this.writeBuffer.length + ); + this.once("drain", () => { + debug("all packets have been sent, closing the transport"); + this.closeTransport(discard); + }); + return; + } + + debug("the buffer is empty, closing the transport right away", discard); + this.closeTransport(discard); + } + + /** + * Closes the underlying transport. + * + * @param {Boolean} discard + * @private + */ + private closeTransport(discard: boolean) { + debug("closing the transport (discard? %s)", discard); + if (discard) this.transport.discard(); + this.transport.close(this.onClose.bind(this, "forced close")); + } +} diff --git a/packages/engine.io/lib/transport.ts b/packages/engine.io/lib/transport.ts new file mode 100644 index 00000000..5e74813c --- /dev/null +++ b/packages/engine.io/lib/transport.ts @@ -0,0 +1,185 @@ +import { EventEmitter } from "events"; +import * as parser_v4 from "engine.io-parser"; +import * as parser_v3 from "./parser-v3/index"; +import debugModule from "debug"; +import type { IncomingMessage, ServerResponse } from "http"; +import { Packet, RawData } from "engine.io-parser"; + +const debug = debugModule("engine:transport"); + +function noop() {} + +type ReadyState = "open" | "closing" | "closed"; + +export type EngineRequest = IncomingMessage & { + _query: Record; + res?: ServerResponse; + cleanup?: Function; + websocket?: any; +}; + +export abstract class Transport extends EventEmitter { + /** + * The session ID. + */ + public sid: string; + /** + * Whether the transport is currently ready to send packets. + */ + public writable = false; + /** + * The revision of the protocol: + * + * - 3 is used in Engine.IO v3 / Socket.IO v2 + * - 4 is used in Engine.IO v4 and above / Socket.IO v3 and above + * + * It is found in the `EIO` query parameters of the HTTP requests. + * + * @see https://github.com/socketio/engine.io-protocol + */ + public protocol: number; + + /** + * The current state of the transport. + * @protected + */ + protected _readyState: ReadyState = "open"; + /** + * Whether the transport is discarded and can be safely closed (used during upgrade). + * @protected + */ + protected discarded = false; + /** + * The parser to use (depends on the revision of the {@link Transport#protocol}. + * @protected + */ + protected parser: any; + /** + * Whether the transport supports binary payloads (else it will be base64-encoded) + * @protected + */ + protected supportsBinary: boolean; + + get readyState() { + return this._readyState; + } + + set readyState(state: ReadyState) { + debug( + "readyState updated from %s to %s (%s)", + this._readyState, + state, + this.name + ); + this._readyState = state; + } + + /** + * Transport constructor. + * + * @param {EngineRequest} req + */ + constructor(req: { _query: Record }) { + super(); + this.protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default + this.parser = this.protocol === 4 ? parser_v4 : parser_v3; + this.supportsBinary = !(req._query && req._query.b64); + } + + /** + * Flags the transport as discarded. + * + * @package + */ + discard() { + this.discarded = true; + } + + /** + * Called with an incoming HTTP request. + * + * @param req + * @package + */ + onRequest(req: any) {} + + /** + * Closes the transport. + * + * @package + */ + close(fn?: () => void) { + if ("closed" === this.readyState || "closing" === this.readyState) return; + + this.readyState = "closing"; + this.doClose(fn || noop); + } + + /** + * Called with a transport error. + * + * @param {String} msg - message error + * @param {Object} desc - error description + * @protected + */ + protected onError(msg: string, desc?) { + if (this.listeners("error").length) { + const err = new Error(msg); + // @ts-ignore + err.type = "TransportError"; + // @ts-ignore + err.description = desc; + this.emit("error", err); + } else { + debug("ignored transport error %s (%s)", msg, desc); + } + } + + /** + * Called with parsed out a packets from the data stream. + * + * @param {Object} packet + * @protected + */ + protected onPacket(packet: Packet) { + this.emit("packet", packet); + } + + /** + * Called with the encoded packet data. + * + * @param {String} data + * @protected + */ + protected onData(data: RawData) { + this.onPacket(this.parser.decodePacket(data)); + } + + /** + * Called upon transport close. + * + * @protected + */ + protected onClose() { + this.readyState = "closed"; + this.emit("close"); + } + + /** + * The name of the transport. + */ + abstract get name(): string; + + /** + * Sends an array of packets. + * + * @param {Array} packets + * @package + */ + abstract send(packets: Packet[]): void; + + /** + * Closes the transport. + */ + abstract doClose(fn?: () => void): void; +} diff --git a/packages/engine.io/lib/transports-uws/index.ts b/packages/engine.io/lib/transports-uws/index.ts new file mode 100644 index 00000000..1c2097eb --- /dev/null +++ b/packages/engine.io/lib/transports-uws/index.ts @@ -0,0 +1,7 @@ +import { Polling } from "./polling"; +import { WebSocket } from "./websocket"; + +export default { + polling: Polling, + websocket: WebSocket, +}; diff --git a/packages/engine.io/lib/transports-uws/polling.ts b/packages/engine.io/lib/transports-uws/polling.ts new file mode 100644 index 00000000..090270ec --- /dev/null +++ b/packages/engine.io/lib/transports-uws/polling.ts @@ -0,0 +1,427 @@ +import { Transport } from "../transport"; +import { createGzip, createDeflate } from "zlib"; +import * as accepts from "accepts"; +import debugModule from "debug"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; + +const debug = debugModule("engine:polling"); + +const compressionMethods = { + gzip: createGzip, + deflate: createDeflate, +}; + +export class Polling extends Transport { + public maxHttpBufferSize: number; + public httpCompression: any; + + private req: HttpRequest & { cleanup: () => void }; + private res: HttpResponse; + private dataReq: HttpRequest; + private dataRes: HttpResponse; + private shouldClose: Function; + + private readonly closeTimeout: number; + + /** + * HTTP polling constructor. + */ + constructor(req) { + super(req); + + this.closeTimeout = 30 * 1000; + } + + /** + * Transport name + */ + get name() { + return "polling"; + } + + /** + * Overrides onRequest. + * + * @param req + * + * @private + */ + onRequest(req) { + const res = req.res; + // remove the reference to the ServerResponse object (as the first request of the session is kept in memory by default) + req.res = null; + + if (req.getMethod() === "get") { + this.onPollRequest(req, res); + } else if (req.getMethod() === "post") { + this.onDataRequest(req, res); + } else { + res.writeStatus("500 Internal Server Error"); + res.end(); + } + } + + /** + * The client sends a request awaiting for us to send data. + * + * @private + */ + onPollRequest(req, res) { + if (this.req) { + debug("request overlap"); + // assert: this.res, '.req and .res should be (un)set together' + this.onError("overlap from client"); + res.writeStatus("500 Internal Server Error"); + res.end(); + return; + } + + debug("setting request"); + + this.req = req; + this.res = res; + + const onClose = () => { + this.writable = false; + this.onError("poll connection closed prematurely"); + }; + + const cleanup = () => { + this.req = this.res = null; + }; + + req.cleanup = cleanup; + res.onAborted(onClose); + + this.writable = true; + this.emit("ready"); + + // if we're still writable but had a pending close, trigger an empty send + if (this.writable && this.shouldClose) { + debug("triggering empty send to append close packet"); + this.send([{ type: "noop" }]); + } + } + + /** + * The client sends a request with data. + * + * @private + */ + onDataRequest(req, res) { + if (this.dataReq) { + // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together' + this.onError("data request overlap from client"); + res.writeStatus("500 Internal Server Error"); + res.end(); + 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) { + return this.onError("invalid content"); + } + + this.dataReq = req; + this.dataRes = res; + + let buffer; + let offset = 0; + + const headers = { + // text/html is required instead of text/plain to avoid an + // unwanted download dialog on certain user-agents (GH-43) + "Content-Type": "text/html", + }; + + this.headers(req, headers); + for (let key in headers) { + res.writeHeader(key, String(headers[key])); + } + + const onEnd = (buffer) => { + this.onData(buffer.toString()); + this.onDataRequestCleanup(); + res.cork(() => { + res.end("ok"); + }); + }; + + res.onAborted(() => { + this.onDataRequestCleanup(); + this.onError("data request connection closed prematurely"); + }); + + 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) { + 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. + * + * @private + */ + private onDataRequestCleanup() { + this.dataReq = this.dataRes = null; + } + + /** + * Processes the incoming data payload. + * + * @param {String} encoded payload + * @private + */ + onData(data) { + debug('received "%s"', data); + const callback = (packet) => { + if ("close" === packet.type) { + debug("got xhr close packet"); + this.onClose(); + return false; + } + + this.onPacket(packet); + }; + + if (this.protocol === 3) { + this.parser.decodePayload(data, callback); + } else { + this.parser.decodePayload(data).forEach(callback); + } + } + + /** + * Overrides onClose. + * + * @private + */ + onClose() { + if (this.writable) { + // close pending poll request + this.send([{ type: "noop" }]); + } + super.onClose(); + } + + /** + * Writes a packet payload. + * + * @param {Object} packet + * @private + */ + send(packets) { + this.writable = false; + + if (this.shouldClose) { + debug("appending close packet to payload"); + packets.push({ type: "close" }); + this.shouldClose(); + this.shouldClose = null; + } + + const doWrite = (data) => { + const compress = packets.some((packet) => { + return packet.options && packet.options.compress; + }); + this.write(data, { compress }); + }; + + if (this.protocol === 3) { + this.parser.encodePayload(packets, this.supportsBinary, doWrite); + } else { + this.parser.encodePayload(packets, doWrite); + } + } + + /** + * Writes data as response to poll request. + * + * @param {String} data + * @param {Object} options + * @private + */ + write(data, options) { + debug('writing "%s"', data); + this.doWrite(data, options, () => { + this.req.cleanup(); + this.emit("drain"); + }); + } + + /** + * Performs the write. + * + * @private + */ + doWrite(data, options, callback) { + // explicit UTF-8 is required for pages not served under utf + const isString = typeof data === "string"; + const contentType = isString + ? "text/plain; charset=UTF-8" + : "application/octet-stream"; + + const headers = { + "Content-Type": contentType, + }; + + const respond = (data) => { + this.headers(this.req, headers); + this.res.cork(() => { + Object.keys(headers).forEach((key) => { + this.res.writeHeader(key, String(headers[key])); + }); + this.res.end(data); + }); + callback(); + }; + + if (!this.httpCompression || !options.compress) { + respond(data); + return; + } + + const len = isString ? Buffer.byteLength(data) : data.length; + if (len < this.httpCompression.threshold) { + respond(data); + return; + } + + const encoding = accepts(this.req).encodings(["gzip", "deflate"]); + if (!encoding) { + respond(data); + return; + } + + this.compress(data, encoding, (err, data) => { + if (err) { + this.res.writeStatus("500 Internal Server Error"); + this.res.end(); + callback(err); + return; + } + + headers["Content-Encoding"] = encoding; + respond(data); + }); + } + + /** + * Compresses data. + * + * @private + */ + compress(data, encoding, callback) { + debug("compressing"); + + const buffers = []; + let nread = 0; + + compressionMethods[encoding](this.httpCompression) + .on("error", callback) + .on("data", function (chunk) { + buffers.push(chunk); + nread += chunk.length; + }) + .on("end", function () { + callback(null, Buffer.concat(buffers, nread)); + }) + .end(data); + } + + /** + * Closes the transport. + * + * @private + */ + doClose(fn) { + debug("closing"); + + let closeTimeoutTimer; + + const onClose = () => { + clearTimeout(closeTimeoutTimer); + fn(); + this.onClose(); + }; + + if (this.writable) { + debug("transport writable - closing right away"); + this.send([{ type: "close" }]); + onClose(); + } else if (this.discarded) { + debug("transport discarded - closing right away"); + onClose(); + } else { + debug("transport not writable - buffering orderly close"); + this.shouldClose = onClose; + closeTimeoutTimer = setTimeout(onClose, this.closeTimeout); + } + } + + /** + * Returns headers for a response. + * + * @param req - request + * @param {Object} extra headers + * @private + */ + headers(req, headers) { + headers = headers || {}; + + // prevent XSS warnings on IE + // https://github.com/LearnBoost/socket.io/pull/1333 + const ua = req.headers["user-agent"]; + if (ua && (~ua.indexOf(";MSIE") || ~ua.indexOf("Trident/"))) { + headers["X-XSS-Protection"] = "0"; + } + + headers["cache-control"] = "no-store"; + + this.emit("headers", headers, req); + return headers; + } +} diff --git a/packages/engine.io/lib/transports-uws/websocket.ts b/packages/engine.io/lib/transports-uws/websocket.ts new file mode 100644 index 00000000..aa5e1c93 --- /dev/null +++ b/packages/engine.io/lib/transports-uws/websocket.ts @@ -0,0 +1,83 @@ +import { Transport } from "../transport"; +import debugModule from "debug"; + +const debug = debugModule("engine:ws"); + +export class WebSocket extends Transport { + protected perMessageDeflate: any; + private socket: any; + + /** + * WebSocket transport + * + * @param req + */ + constructor(req) { + super(req); + this.writable = false; + this.perMessageDeflate = null; + } + + /** + * Transport name + */ + get name() { + return "websocket"; + } + + /** + * Advertise upgrade support. + */ + get handlesUpgrades() { + return true; + } + + /** + * Writes a packet payload. + * + * @param {Array} packets + * @private + */ + send(packets) { + this.writable = false; + + for (let i = 0; i < packets.length; i++) { + const packet = packets[i]; + const isLast = i + 1 === packets.length; + + const send = (data) => { + const isBinary = typeof data !== "string"; + const compress = + this.perMessageDeflate && + Buffer.byteLength(data) > this.perMessageDeflate.threshold; + + debug('writing "%s"', data); + this.socket.send(data, isBinary, compress); + + if (isLast) { + this.emit("drain"); + this.writable = true; + this.emit("ready"); + } + }; + + if (packet.options && typeof packet.options.wsPreEncoded === "string") { + send(packet.options.wsPreEncoded); + } else { + this.parser.encodePacket(packet, this.supportsBinary, send); + } + } + } + + /** + * Closes the transport. + * + * @private + */ + doClose(fn) { + debug("closing"); + fn && fn(); + // call fn first since socket.end() immediately emits a "close" event + this.socket.end(); + } +} diff --git a/packages/engine.io/lib/transports/index.ts b/packages/engine.io/lib/transports/index.ts new file mode 100644 index 00000000..e585112b --- /dev/null +++ b/packages/engine.io/lib/transports/index.ts @@ -0,0 +1,24 @@ +import { Polling as XHR } from "./polling"; +import { JSONP } from "./polling-jsonp"; +import { WebSocket } from "./websocket"; +import { WebTransport } from "./webtransport"; + +export default { + polling: polling, + websocket: WebSocket, + webtransport: WebTransport, +}; + +/** + * Polling polymorphic constructor. + */ + +function polling(req) { + if ("string" === typeof req._query.j) { + return new JSONP(req); + } else { + return new XHR(req); + } +} + +polling.upgradesTo = ["websocket", "webtransport"]; diff --git a/packages/engine.io/lib/transports/polling-jsonp.ts b/packages/engine.io/lib/transports/polling-jsonp.ts new file mode 100644 index 00000000..05806008 --- /dev/null +++ b/packages/engine.io/lib/transports/polling-jsonp.ts @@ -0,0 +1,48 @@ +import { Polling } from "./polling"; +import * as qs from "querystring"; +import type { RawData } from "engine.io-parser"; + +const rDoubleSlashes = /\\\\n/g; +const rSlashes = /(\\)?\\n/g; + +export class JSONP extends Polling { + private readonly head: string; + private readonly foot: string; + + /** + * JSON-P polling transport. + */ + constructor(req) { + super(req); + + this.head = "___eio[" + (req._query.j || "").replace(/[^0-9]/g, "") + "]("; + this.foot = ");"; + } + + override onData(data: RawData) { + // we leverage the qs module so that we get built-in DoS protection + // and the fast alternative to decodeURIComponent + data = qs.parse(data).d as string; + if ("string" === typeof data) { + // client will send already escaped newlines as \\\\n and newlines as \\n + // \\n must be replaced with \n and \\\\n with \\n + data = data.replace(rSlashes, function (match, slashes) { + return slashes ? match : "\n"; + }); + super.onData(data.replace(rDoubleSlashes, "\\n")); + } + } + + override doWrite(data, options, callback) { + // we must output valid javascript, not valid json + // see: http://timelessrepo.com/json-isnt-a-javascript-subset + const js = JSON.stringify(data) + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029"); + + // prepare response + data = this.head + js + this.foot; + + super.doWrite(data, options, callback); + } +} diff --git a/packages/engine.io/lib/transports/polling.ts b/packages/engine.io/lib/transports/polling.ts new file mode 100644 index 00000000..1f463359 --- /dev/null +++ b/packages/engine.io/lib/transports/polling.ts @@ -0,0 +1,389 @@ +import { EngineRequest, Transport } from "../transport"; +import { createGzip, createDeflate } from "zlib"; +import * as accepts from "accepts"; +import debugModule from "debug"; +import type { IncomingMessage, ServerResponse } from "http"; +import type { Packet, RawData } from "engine.io-parser"; + +const debug = debugModule("engine:polling"); + +const compressionMethods = { + gzip: createGzip, + deflate: createDeflate, +}; + +export class Polling extends Transport { + public maxHttpBufferSize: number; + public httpCompression: any; + + private req: EngineRequest; + private res: ServerResponse; + private dataReq: IncomingMessage; + private dataRes: ServerResponse; + private shouldClose: () => void; + + private readonly closeTimeout: number; + + /** + * HTTP polling constructor. + */ + constructor(req) { + super(req); + + this.closeTimeout = 30 * 1000; + } + + /** + * Transport name + */ + get name() { + return "polling"; + } + + /** + * Overrides onRequest. + * + * @param {EngineRequest} req + * @package + */ + onRequest(req: EngineRequest) { + const res = req.res; + // remove the reference to the ServerResponse object (as the first request of the session is kept in memory by default) + req.res = null; + + if ("GET" === req.method) { + this.onPollRequest(req, res); + } else if ("POST" === req.method) { + this.onDataRequest(req, res); + } else { + res.writeHead(500); + res.end(); + } + } + + /** + * The client sends a request awaiting for us to send data. + * + * @private + */ + private onPollRequest(req: EngineRequest, res: ServerResponse) { + if (this.req) { + debug("request overlap"); + // assert: this.res, '.req and .res should be (un)set together' + this.onError("overlap from client"); + res.writeHead(400); + res.end(); + return; + } + + debug("setting request"); + + this.req = req; + this.res = res; + + const onClose = () => { + this.onError("poll connection closed prematurely"); + }; + + const cleanup = () => { + req.removeListener("close", onClose); + this.req = this.res = null; + }; + + req.cleanup = cleanup; + req.on("close", onClose); + + this.writable = true; + this.emit("ready"); + + // if we're still writable but had a pending close, trigger an empty send + if (this.writable && this.shouldClose) { + debug("triggering empty send to append close packet"); + this.send([{ type: "noop" }]); + } + } + + /** + * The client sends a request with data. + * + * @private + */ + private onDataRequest(req: IncomingMessage, res: ServerResponse) { + if (this.dataReq) { + // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together' + this.onError("data request overlap from client"); + res.writeHead(400); + res.end(); + return; + } + + const isBinary = "application/octet-stream" === req.headers["content-type"]; + + if (isBinary && this.protocol === 4) { + return this.onError("invalid content"); + } + + this.dataReq = req; + this.dataRes = res; + + let chunks = isBinary ? Buffer.concat([]) : ""; + + const cleanup = () => { + req.removeListener("data", onData); + req.removeListener("end", onEnd); + req.removeListener("close", onClose); + this.dataReq = this.dataRes = chunks = null; + }; + + const onClose = () => { + cleanup(); + this.onError("data request connection closed prematurely"); + }; + + const onData = (data) => { + let contentLength; + if (isBinary) { + chunks = Buffer.concat([chunks, data]); + contentLength = chunks.length; + } else { + chunks += data; + contentLength = Buffer.byteLength(chunks); + } + + if (contentLength > this.maxHttpBufferSize) { + res.writeHead(413).end(); + cleanup(); + } + }; + + const onEnd = () => { + this.onData(chunks); + + const headers = { + // text/html is required instead of text/plain to avoid an + // unwanted download dialog on certain user-agents (GH-43) + "Content-Type": "text/html", + "Content-Length": "2", + }; + + res.writeHead(200, this.headers(req, headers)); + res.end("ok"); + cleanup(); + }; + + req.on("close", onClose); + if (!isBinary) req.setEncoding("utf8"); + req.on("data", onData); + req.on("end", onEnd); + } + + /** + * Processes the incoming data payload. + * + * @param data - encoded payload + * @protected + */ + override onData(data: RawData) { + debug('received "%s"', data); + const callback = (packet) => { + if ("close" === packet.type) { + debug("got xhr close packet"); + this.onClose(); + return false; + } + + this.onPacket(packet); + }; + + if (this.protocol === 3) { + this.parser.decodePayload(data, callback); + } else { + this.parser.decodePayload(data).forEach(callback); + } + } + + /** + * Overrides onClose. + * + * @private + */ + onClose() { + if (this.writable) { + // close pending poll request + this.send([{ type: "noop" }]); + } + super.onClose(); + } + + send(packets: Packet[]) { + this.writable = false; + + if (this.shouldClose) { + debug("appending close packet to payload"); + packets.push({ type: "close" }); + this.shouldClose(); + this.shouldClose = null; + } + + const doWrite = (data) => { + const compress = packets.some((packet) => { + return packet.options && packet.options.compress; + }); + this.write(data, { compress }); + }; + + if (this.protocol === 3) { + this.parser.encodePayload(packets, this.supportsBinary, doWrite); + } else { + this.parser.encodePayload(packets, doWrite); + } + } + + /** + * Writes data as response to poll request. + * + * @param {String} data + * @param {Object} options + * @private + */ + private write(data, options) { + debug('writing "%s"', data); + this.doWrite(data, options, () => { + this.req.cleanup(); + this.emit("drain"); + }); + } + + /** + * Performs the write. + * + * @protected + */ + protected doWrite(data, options, callback) { + // explicit UTF-8 is required for pages not served under utf + const isString = typeof data === "string"; + const contentType = isString + ? "text/plain; charset=UTF-8" + : "application/octet-stream"; + + const headers = { + "Content-Type": contentType, + }; + + const respond = (data) => { + headers["Content-Length"] = + "string" === typeof data ? Buffer.byteLength(data) : data.length; + this.res.writeHead(200, this.headers(this.req, headers)); + this.res.end(data); + callback(); + }; + + if (!this.httpCompression || !options.compress) { + respond(data); + return; + } + + const len = isString ? Buffer.byteLength(data) : data.length; + if (len < this.httpCompression.threshold) { + respond(data); + return; + } + + const encoding = accepts(this.req).encodings(["gzip", "deflate"]); + if (!encoding) { + respond(data); + return; + } + + this.compress(data, encoding, (err, data) => { + if (err) { + this.res.writeHead(500); + this.res.end(); + callback(err); + return; + } + + headers["Content-Encoding"] = encoding; + respond(data); + }); + } + + /** + * Compresses data. + * + * @private + */ + private compress(data, encoding, callback) { + debug("compressing"); + + const buffers = []; + let nread = 0; + + compressionMethods[encoding](this.httpCompression) + .on("error", callback) + .on("data", function (chunk) { + buffers.push(chunk); + nread += chunk.length; + }) + .on("end", function () { + callback(null, Buffer.concat(buffers, nread)); + }) + .end(data); + } + + /** + * Closes the transport. + * + * @private + */ + override doClose(fn: () => void) { + debug("closing"); + + let closeTimeoutTimer; + + if (this.dataReq) { + debug("aborting ongoing data request"); + this.dataReq.destroy(); + } + + const onClose = () => { + clearTimeout(closeTimeoutTimer); + fn(); + this.onClose(); + }; + + if (this.writable) { + debug("transport writable - closing right away"); + this.send([{ type: "close" }]); + onClose(); + } else if (this.discarded) { + debug("transport discarded - closing right away"); + onClose(); + } else { + debug("transport not writable - buffering orderly close"); + this.shouldClose = onClose; + closeTimeoutTimer = setTimeout(onClose, this.closeTimeout); + } + } + + /** + * Returns headers for a response. + * + * @param {http.IncomingMessage} req + * @param {Object} headers - extra headers + * @private + */ + private headers(req: IncomingMessage, headers: Record = {}) { + // prevent XSS warnings on IE + // https://github.com/LearnBoost/socket.io/pull/1333 + const ua = req.headers["user-agent"]; + if (ua && (~ua.indexOf(";MSIE") || ~ua.indexOf("Trident/"))) { + headers["X-XSS-Protection"] = "0"; + } + + headers["cache-control"] = "no-store"; + + this.emit("headers", headers, req); + return headers; + } +} diff --git a/packages/engine.io/lib/transports/websocket.ts b/packages/engine.io/lib/transports/websocket.ts new file mode 100644 index 00000000..71ac94a7 --- /dev/null +++ b/packages/engine.io/lib/transports/websocket.ts @@ -0,0 +1,112 @@ +import { EngineRequest, Transport } from "../transport"; +import debugModule from "debug"; +import type { Packet, RawData } from "engine.io-parser"; + +const debug = debugModule("engine:ws"); + +export class WebSocket extends Transport { + protected perMessageDeflate: any; + private socket: any; + + /** + * WebSocket transport + * + * @param {EngineRequest} req + */ + constructor(req: EngineRequest) { + super(req); + this.socket = req.websocket; + this.socket.on("message", (data, isBinary) => { + const message = isBinary ? data : data.toString(); + debug('received "%s"', message); + super.onData(message); + }); + this.socket.once("close", this.onClose.bind(this)); + this.socket.on("error", this.onError.bind(this)); + this.writable = true; + this.perMessageDeflate = null; + } + + /** + * Transport name + */ + get name() { + return "websocket"; + } + + /** + * Advertise upgrade support. + */ + get handlesUpgrades() { + return true; + } + + send(packets: Packet[]) { + this.writable = false; + + for (let i = 0; i < packets.length; i++) { + const packet = packets[i]; + const isLast = i + 1 === packets.length; + + if (this._canSendPreEncodedFrame(packet)) { + // the WebSocket frame was computed with WebSocket.Sender.frame() + // see https://github.com/websockets/ws/issues/617#issuecomment-283002469 + this.socket._sender.sendFrame( + // @ts-ignore + packet.options.wsPreEncodedFrame, + isLast ? this._onSentLast : this._onSent + ); + } else { + this.parser.encodePacket( + packet, + this.supportsBinary, + isLast ? this._doSendLast : this._doSend + ); + } + } + } + + /** + * Whether the encoding of the WebSocket frame can be skipped. + * @param packet + * @private + */ + private _canSendPreEncodedFrame(packet: Packet) { + return ( + !this.perMessageDeflate && + typeof this.socket?._sender?.sendFrame === "function" && + // @ts-ignore + packet.options?.wsPreEncodedFrame !== undefined + ); + } + + private _doSend = (data: RawData) => { + this.socket.send(data, this._onSent); + }; + + private _doSendLast = (data: RawData) => { + this.socket.send(data, this._onSentLast); + }; + + private _onSent = (err?: Error) => { + if (err) { + this.onError("write error", err.stack); + } + }; + + private _onSentLast = (err?: Error) => { + if (err) { + this.onError("write error", err.stack); + } else { + this.emit("drain"); + this.writable = true; + this.emit("ready"); + } + }; + + doClose(fn?: () => void) { + debug("closing"); + this.socket.close(); + fn && fn(); + } +} diff --git a/packages/engine.io/lib/transports/webtransport.ts b/packages/engine.io/lib/transports/webtransport.ts new file mode 100644 index 00000000..07852282 --- /dev/null +++ b/packages/engine.io/lib/transports/webtransport.ts @@ -0,0 +1,69 @@ +import { Transport } from "../transport"; +import debugModule from "debug"; +import { createPacketEncoderStream } from "engine.io-parser"; + +const debug = debugModule("engine:webtransport"); + +/** + * Reference: https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API + */ +export class WebTransport extends Transport { + private readonly writer; + + constructor(private readonly session, stream, reader) { + super({ _query: { EIO: "4" } }); + + const transformStream = createPacketEncoderStream(); + transformStream.readable.pipeTo(stream.writable).catch(() => { + debug("the stream was closed"); + }); + this.writer = transformStream.writable.getWriter(); + + (async () => { + try { + while (true) { + const { value, done } = await reader.read(); + if (done) { + debug("session is closed"); + break; + } + debug("received chunk: %o", value); + this.onPacket(value); + } + } catch (e) { + debug("error while reading: %s", e.message); + } + })(); + + session.closed.then(() => this.onClose()); + + this.writable = true; + } + + get name() { + return "webtransport"; + } + + async send(packets) { + this.writable = false; + + try { + for (let i = 0; i < packets.length; i++) { + const packet = packets[i]; + await this.writer.write(packet); + } + } catch (e) { + debug("error while writing: %s", e.message); + } + + this.emit("drain"); + this.writable = true; + this.emit("ready"); + } + + doClose(fn) { + debug("closing WebTransport session"); + this.session.close(); + fn && fn(); + } +} diff --git a/packages/engine.io/lib/userver.ts b/packages/engine.io/lib/userver.ts new file mode 100644 index 00000000..ce56575a --- /dev/null +++ b/packages/engine.io/lib/userver.ts @@ -0,0 +1,358 @@ +import debugModule from "debug"; +import { AttachOptions, BaseServer, Server } from "./server"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import transports from "./transports-uws"; + +const debug = debugModule("engine:uws"); + +export interface uOptions { + /** + * What permessage-deflate compression to use. uWS.DISABLED, uWS.SHARED_COMPRESSOR or any of the uWS.DEDICATED_COMPRESSOR_xxxKB. + * @default uWS.DISABLED + */ + compression?: number; + /** + * Maximum amount of seconds that may pass without sending or getting a message. Connection is closed if this timeout passes. Resolution (granularity) for timeouts are typically 4 seconds, rounded to closest. Disable by using 0. + * @default 120 + */ + idleTimeout?: number; + /** + * Maximum length of allowed backpressure per socket when publishing or sending messages. Slow receivers with too high backpressure will be skipped until they catch up or timeout. + * @default 1024 * 1024 + */ + maxBackpressure?: number; +} + +export class uServer extends BaseServer { + protected init() {} + protected cleanup() {} + + /** + * Prepares a request by processing the query string. + * + * @private + */ + private prepare(req, res: HttpResponse) { + req.method = req.getMethod().toUpperCase(); + req.url = req.getUrl(); + + const params = new URLSearchParams(req.getQuery()); + req._query = Object.fromEntries(params.entries()); + + req.headers = {}; + req.forEach((key, value) => { + req.headers[key] = value; + }); + + req.connection = { + remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(), + }; + + res.onAborted(() => { + debug("response has been aborted"); + }); + } + + protected createTransport(transportName, req) { + return new transports[transportName](req); + } + + /** + * Attach the engine to a µWebSockets.js server + * @param app + * @param options + */ + public attach( + app /* : TemplatedApp */, + options: AttachOptions & uOptions = {} + ) { + const path = this._computePath(options); + (app as TemplatedApp) + .any(path, this.handleRequest.bind(this)) + // + .ws<{ transport: any }>(path, { + compression: options.compression, + idleTimeout: options.idleTimeout, + maxBackpressure: options.maxBackpressure, + maxPayloadLength: this.opts.maxHttpBufferSize, + upgrade: this.handleUpgrade.bind(this), + open: (ws) => { + const transport = ws.getUserData().transport; + transport.socket = ws; + transport.writable = true; + transport.emit("ready"); + }, + message: (ws, message, isBinary) => { + ws.getUserData().transport.onData( + isBinary ? message : Buffer.from(message).toString() + ); + }, + close: (ws, code, message) => { + ws.getUserData().transport.onClose(code, message); + }, + }); + } + + override _applyMiddlewares( + req: any, + res: any, + callback: (err?: any) => void + ): void { + if (this.middlewares.length === 0) { + return callback(); + } + + // needed to buffer headers until the status is computed + req.res = new ResponseWrapper(res); + + super._applyMiddlewares(req, req.res, (err) => { + // some middlewares (like express-session) wait for the writeHead() call to flush their headers + // see https://github.com/expressjs/session/blob/1010fadc2f071ddf2add94235d72224cf65159c6/index.js#L220-L244 + req.res.writeHead(); + + callback(err); + }); + } + + private handleRequest( + res: HttpResponse, + req: HttpRequest & { res: any; _query: any } + ) { + debug('handling "%s" http request "%s"', req.getMethod(), req.getUrl()); + this.prepare(req, res); + + req.res = res; + + const callback = (errorCode, errorContext) => { + if (errorCode !== undefined) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + this.abortRequest(req.res, errorCode, errorContext); + return; + } + + if (req._query.sid) { + debug("setting new request for existing client"); + // @ts-ignore + this.clients[req._query.sid].transport.onRequest(req); + } else { + const closeConnection = (errorCode, errorContext) => + this.abortRequest(res, errorCode, errorContext); + this.handshake(req._query.transport, req, closeConnection); + } + }; + + this._applyMiddlewares(req, res, (err) => { + if (err) { + callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" }); + } else { + this.verify(req, false, callback); + } + }); + } + + private handleUpgrade( + res: HttpResponse, + req: HttpRequest & { res: any; _query: any }, + context + ) { + debug("on upgrade"); + + this.prepare(req, res); + + req.res = res; + + const callback = async (errorCode, errorContext) => { + if (errorCode !== undefined) { + this.emit("connection_error", { + req, + code: errorCode, + message: Server.errorMessages[errorCode], + context: errorContext, + }); + this.abortRequest(res, errorCode, errorContext); + return; + } + + const id = req._query.sid; + let transport; + + if (id) { + const client = this.clients[id]; + if (!client) { + debug("upgrade attempt for closed client"); + res.close(); + } else if (client.upgrading) { + debug("transport has already been trying to upgrade"); + res.close(); + } else if (client.upgraded) { + debug("transport had already been upgraded"); + res.close(); + } else { + debug("upgrading existing transport"); + transport = this.createTransport(req._query.transport, req); + client._maybeUpgrade(transport); + } + } else { + transport = await this.handshake( + req._query.transport, + req, + (errorCode, errorContext) => + this.abortRequest(res, errorCode, errorContext) + ); + if (!transport) { + return; + } + } + + // calling writeStatus() triggers the flushing of any header added in a middleware + req.res.writeStatus("101 Switching Protocols"); + + res.upgrade( + { + transport, + }, + req.getHeader("sec-websocket-key"), + req.getHeader("sec-websocket-protocol"), + req.getHeader("sec-websocket-extensions"), + context + ); + }; + + this._applyMiddlewares(req, res, (err) => { + if (err) { + callback(Server.errors.BAD_REQUEST, { name: "MIDDLEWARE_FAILURE" }); + } else { + this.verify(req, true, callback); + } + }); + } + + private abortRequest( + res: HttpResponse | ResponseWrapper, + errorCode, + errorContext + ) { + const statusCode = + errorCode === Server.errors.FORBIDDEN + ? "403 Forbidden" + : "400 Bad Request"; + const message = + errorContext && errorContext.message + ? errorContext.message + : Server.errorMessages[errorCode]; + + res.writeStatus(statusCode); + res.writeHeader("Content-Type", "application/json"); + res.end( + JSON.stringify({ + code: errorCode, + message, + }) + ); + } +} + +class ResponseWrapper { + private statusWritten: boolean = false; + private headers = []; + private isAborted = false; + + constructor(readonly res: HttpResponse) {} + + public set statusCode(status: number) { + if (!status) { + return; + } + // FIXME: handle all status codes? + this.writeStatus(status === 200 ? "200 OK" : "204 No Content"); + } + + public writeHead(status: number) { + this.statusCode = status; + } + + public setHeader(key, value) { + if (Array.isArray(value)) { + value.forEach((val) => { + this.writeHeader(key, val); + }); + } else { + this.writeHeader(key, value); + } + } + + public removeHeader() { + // FIXME: not implemented + } + + // needed by vary: https://github.com/jshttp/vary/blob/5d725d059b3871025cf753e9dfa08924d0bcfa8f/index.js#L134 + public getHeader() {} + + public writeStatus(status: string) { + if (this.isAborted) return; + + this.res.writeStatus(status); + this.statusWritten = true; + this.writeBufferedHeaders(); + return this; + } + + public writeHeader(key: string, value: string) { + if (this.isAborted) return; + + if (key === "Content-Length") { + // the content length is automatically added by uWebSockets.js + return; + } + if (this.statusWritten) { + this.res.writeHeader(key, value); + } else { + this.headers.push([key, value]); + } + } + + private writeBufferedHeaders() { + this.headers.forEach(([key, value]) => { + this.res.writeHeader(key, value); + }); + } + + public end(data) { + if (this.isAborted) return; + + this.res.cork(() => { + if (!this.statusWritten) { + // status will be inferred as "200 OK" + this.writeBufferedHeaders(); + } + this.res.end(data); + }); + } + + public onData(fn) { + if (this.isAborted) return; + + this.res.onData(fn); + } + + public onAborted(fn) { + if (this.isAborted) return; + + this.res.onAborted(() => { + // Any attempt to use the UWS response object after abort will throw! + this.isAborted = true; + fn(); + }); + } + + public cork(fn) { + if (this.isAborted) return; + + this.res.cork(fn); + } +} diff --git a/packages/engine.io/package-lock.json b/packages/engine.io/package-lock.json new file mode 100644 index 00000000..cad4c345 --- /dev/null +++ b/packages/engine.io/package-lock.json @@ -0,0 +1,4485 @@ +{ + "name": "engine.io", + "version": "6.5.4", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "engine.io", + "version": "6.5.4", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "devDependencies": { + "@fails-components/webtransport": "^0.1.7", + "babel-eslint": "^8.0.2", + "eiows": "^4.1.2", + "engine.io-client": "6.5.0", + "engine.io-client-v3": "npm:engine.io-client@3.5.2", + "expect.js": "^0.3.1", + "express-session": "^1.17.3", + "helmet": "^6.0.1", + "mocha": "^9.1.3", + "node-forge": "^1.3.1", + "prettier": "^2.8.2", + "rimraf": "^3.0.2", + "superagent": "^3.8.1", + "typescript": "^4.4.3", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz", + "integrity": "sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g==", + "dev": true, + "dependencies": { + "@babel/highlight": "7.0.0-beta.44" + } + }, + "node_modules/@babel/generator": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz", + "integrity": "sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ==", + "dev": true, + "dependencies": { + "@babel/types": "7.0.0-beta.44", + "jsesc": "^2.5.1", + "lodash": "^4.2.0", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz", + "integrity": "sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "7.0.0-beta.44", + "@babel/template": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz", + "integrity": "sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw==", + "dev": true, + "dependencies": { + "@babel/types": "7.0.0-beta.44" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", + "integrity": "sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA==", + "dev": true, + "dependencies": { + "@babel/types": "7.0.0-beta.44" + } + }, + "node_modules/@babel/highlight": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz", + "integrity": "sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ==", + "dev": true, + "dependencies": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^3.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz", + "integrity": "sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "lodash": "^4.2.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz", + "integrity": "sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/generator": "7.0.0-beta.44", + "@babel/helper-function-name": "7.0.0-beta.44", + "@babel/helper-split-export-declaration": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "debug": "^3.1.0", + "globals": "^11.1.0", + "invariant": "^2.2.0", + "lodash": "^4.2.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@babel/types": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz", + "integrity": "sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2", + "lodash": "^4.2.0", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@fails-components/webtransport": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@fails-components/webtransport/-/webtransport-0.1.7.tgz", + "integrity": "sha512-RD8kGxFVSBElx7Y/ApskD1/t8kXF4GNtvveJnnMET8TAd6FfcEmtETvzJax5o7KyvGONsoVlCtLRY6s12ncn4w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "node_modules/@types/node": { + "version": "16.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/babel-eslint": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.2.6.tgz", + "integrity": "sha512-aCdHjhzcILdP8c9lej7hvXKvQieyRt20SF102SIGyY4cUIiw6UaAtK4j2o3dXX74jEmy0TJ0CEhv4fTIM3SzcA==", + "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/traverse": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babylon": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eiows": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/eiows/-/eiows-4.1.2.tgz", + "integrity": "sha512-dNu+yOdp/mPROgEIW9pFnx+iBLob130BPqspXxSt63OvICtitrpYQSmdJ2M7irIwqh3oj1JfDKadHPCocKPBpg==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=14.13.0 <19.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.0.tgz", + "integrity": "sha512-C7eN3OKggSfd5g8IDgUA9guC8TNS6CEganKT7dL6Fp3q+FobcQ/WBn2Qq2XTL1vNTiFZfDzXohvqLuR9dWejdg==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.1.0", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client-v3": { + "name": "engine.io-client", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "dev": true, + "dependencies": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + } + }, + "node_modules/engine.io-client-v3/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/engine.io-client-v3/node_modules/engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "dev": true, + "dependencies": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "node_modules/engine.io-client-v3/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/engine.io-client-v3/node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-client-v3/node_modules/xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/engine.io-client/node_modules/engine.io-parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", + "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dev": true, + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", + "dev": true, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "dependencies": { + "isarray": "2.0.1" + } + }, + "node_modules/has-binary2/node_modules/isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + }, + "node_modules/has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dependencies": { + "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.43.0.tgz", + "integrity": "sha512-QB0MMv+tn9Ur2DtJrc8y09n0n6sw88CyDniWSX2cHW10goQXYPK9ZpFJOktDS4ron501edPX6h9i7Pg+RnH5nQ==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true + }, + "node_modules/parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prettier": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz", + "integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "deprecated": "Please upgrade to v7.0.2+ of superagent. We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing. See the releases tab for more information at . Thanks to @shadowgate15, @spence-s, and @niftylettuce. Superagent is sponsored by Forward Email at .", + "dev": true, + "dependencies": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/uWebSockets.js": { + "version": "20.30.0", + "resolved": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#d39d4181daf5b670d44cbc1b18f8c28c85fd4142", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz", + "integrity": "sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g==", + "dev": true, + "requires": { + "@babel/highlight": "7.0.0-beta.44" + } + }, + "@babel/generator": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz", + "integrity": "sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44", + "jsesc": "^2.5.1", + "lodash": "^4.2.0", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz", + "integrity": "sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.44", + "@babel/template": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz", + "integrity": "sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", + "integrity": "sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/highlight": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz", + "integrity": "sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^3.0.0" + } + }, + "@babel/template": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz", + "integrity": "sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "lodash": "^4.2.0" + } + }, + "@babel/traverse": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz", + "integrity": "sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/generator": "7.0.0-beta.44", + "@babel/helper-function-name": "7.0.0-beta.44", + "@babel/helper-split-export-declaration": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "debug": "^3.1.0", + "globals": "^11.1.0", + "invariant": "^2.2.0", + "lodash": "^4.2.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "@babel/types": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz", + "integrity": "sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.2.0", + "to-fast-properties": "^2.0.0" + } + }, + "@fails-components/webtransport": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@fails-components/webtransport/-/webtransport-0.1.7.tgz", + "integrity": "sha512-RD8kGxFVSBElx7Y/ApskD1/t8kXF4GNtvveJnnMET8TAd6FfcEmtETvzJax5o7KyvGONsoVlCtLRY6s12ncn4w==", + "dev": true, + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.1" + } + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/node": { + "version": "16.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.3.tgz", + "integrity": "sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==" + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "babel-eslint": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.2.6.tgz", + "integrity": "sha512-aCdHjhzcILdP8c9lej7hvXKvQieyRt20SF102SIGyY4cUIiw6UaAtK4j2o3dXX74jEmy0TJ0CEhv4fTIM3SzcA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/traverse": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + } + }, + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "eiows": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/eiows/-/eiows-4.1.2.tgz", + "integrity": "sha512-dNu+yOdp/mPROgEIW9pFnx+iBLob130BPqspXxSt63OvICtitrpYQSmdJ2M7irIwqh3oj1JfDKadHPCocKPBpg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "engine.io-client": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.0.tgz", + "integrity": "sha512-C7eN3OKggSfd5g8IDgUA9guC8TNS6CEganKT7dL6Fp3q+FobcQ/WBn2Qq2XTL1vNTiFZfDzXohvqLuR9dWejdg==", + "dev": true, + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.1.0", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "engine.io-parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", + "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "dev": true + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + } + } + }, + "engine.io-client-v3": { + "version": "npm:engine.io-client@3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "dev": true, + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "xmlhttprequest-ssl": "~1.6.2", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "dev": true, + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true, + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", + "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "dev": true + } + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dev": true, + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "dev": true, + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", + "dev": true + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "node-abi": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.43.0.tgz", + "integrity": "sha512-QB0MMv+tn9Ur2DtJrc8y09n0n6sw88CyDniWSX2cHW10goQXYPK9ZpFJOktDS4ron501edPX6h9i7Pg+RnH5nQ==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==", + "dev": true + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==", + "dev": true + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "prettier": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz", + "integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "typescript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "dev": true + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "requires": { + "random-bytes": "~1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uWebSockets.js": { + "version": "git+ssh://git@github.com/uNetworking/uWebSockets.js.git#d39d4181daf5b670d44cbc1b18f8c28c85fd4142", + "dev": true, + "from": "uWebSockets.js@github:uNetworking/uWebSockets.js#v20.30.0" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/packages/engine.io/package.json b/packages/engine.io/package.json new file mode 100644 index 00000000..b20c90f8 --- /dev/null +++ b/packages/engine.io/package.json @@ -0,0 +1,85 @@ +{ + "name": "engine.io", + "version": "6.6.0", + "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", + "types": "./build/engine.io.d.ts", + "exports": { + "types": "./build/engine.io.d.ts", + "import": "./wrapper.mjs", + "require": "./build/engine.io.js" + }, + "author": "Guillermo Rauch ", + "homepage": "https://github.com/socketio/engine.io", + "contributors": [ + { + "name": "Eugen Dueck", + "web": "https://github.com/EugenDueck" + }, + { + "name": "Afshin Mehrabani", + "web": "https://github.com/afshinm" + }, + { + "name": "Christoph Dorn", + "web": "https://github.com/cadorn" + }, + { + "name": "Mark Mokryn", + "email": "mokesmokes@gmail.com" + } + ], + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "devDependencies": { + "@fails-components/webtransport": "^0.1.7", + "babel-eslint": "^8.0.2", + "eiows": "^4.1.2", + "engine.io-client": "6.5.0", + "engine.io-client-v3": "npm:engine.io-client@3.5.2", + "expect.js": "^0.3.1", + "express-session": "^1.17.3", + "helmet": "^6.0.1", + "mocha": "^9.1.3", + "node-forge": "^1.3.1", + "prettier": "^2.8.2", + "rimraf": "^3.0.2", + "superagent": "^3.8.1", + "typescript": "^4.4.3", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.30.0" + }, + "scripts": { + "compile": "rimraf ./build && tsc", + "test": "npm run compile && npm run format:check && npm run test:default && npm run test:compat-v3", + "test:default": "mocha --bail --exit", + "test:compat-v3": "EIO_CLIENT=3 mocha --exit", + "test:eiows": "EIO_WS_ENGINE=eiows mocha --exit", + "test:uws": "EIO_WS_ENGINE=uws mocha --exit", + "format:check": "prettier --check \"wrapper.mjs\" \"lib/**/*.ts\" \"test/**/*.js\" \"test/webtransport.mjs\"", + "format:fix": "prettier --write \"wrapper.mjs\" \"lib/**/*.ts\" \"test/**/*.js\" \"test/webtransport.mjs\"", + "prepack": "npm run compile" + }, + "repository": { + "type": "git", + "url": "git@github.com:socketio/engine.io.git" + }, + "files": [ + "build/", + "wrapper.mjs" + ], + "engines": { + "node": ">=10.2.0" + } +} diff --git a/packages/engine.io/test/.eslintrc.json b/packages/engine.io/test/.eslintrc.json new file mode 100644 index 00000000..7eeefc33 --- /dev/null +++ b/packages/engine.io/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/packages/engine.io/test/common.js b/packages/engine.io/test/common.js new file mode 100644 index 00000000..72b254a3 --- /dev/null +++ b/packages/engine.io/test/common.js @@ -0,0 +1,57 @@ +const { listen, uServer } = require(".."); +const { Socket } = + process.env.EIO_CLIENT === "3" + ? require("engine.io-client-v3") + : require("engine.io-client"); + +/** + * Listen shortcut that fires a callback on an ephemeral port. + */ + +exports.listen = (opts, fn) => { + if ("function" === typeof opts) { + fn = opts; + opts = {}; + } + + opts.allowEIO3 = true; + + if (process.env.EIO_WS_ENGINE === "uws") { + const { App, us_socket_local_port } = require("uWebSockets.js"); + const engine = new uServer(opts); + const app = App(); + engine.attach(app, opts); + + app.listen(0, (listenSocket) => { + const port = us_socket_local_port(listenSocket); + process.nextTick(() => { + fn(port); + }); + }); + + return engine; + } + + if (process.env.EIO_WS_ENGINE === "eiows") { + opts.wsEngine = require("eiows").Server; + } + + const e = listen(0, opts, () => { + fn(e.httpServer.address().port); + }); + + return e; +}; + +exports.ClientSocket = Socket; + +exports.createPartialDone = (done, count) => { + let i = 0; + return () => { + if (++i === count) { + done(); + } else if (i > count) { + done(new Error(`partialDone() called too many times: ${i} > ${count}`)); + } + }; +}; diff --git a/packages/engine.io/test/engine.io.js b/packages/engine.io/test/engine.io.js new file mode 100644 index 00000000..75d1748e --- /dev/null +++ b/packages/engine.io/test/engine.io.js @@ -0,0 +1,253 @@ +const net = require("net"); +const { Server, protocol, attach } = require(".."); +const listen = require("./common").listen; +const expect = require("expect.js"); +const request = require("superagent"); +const http = require("http"); + +/** + * Tests. + */ + +describe("engine", () => { + it("should expose protocol number", () => { + expect(protocol).to.be.a("number"); + }); + + it.skip("should be the same version as client", () => { + const version = require("../package.json").version; + expect(version).to.be(require("engine.io-client/package.json").version); + }); + + describe("engine()", () => { + it("should create a Server when require called with no arguments", () => { + const engine = new Server(); + expect(engine).to.be.an(Server); + expect(engine.ws).to.be.ok(); + }); + + it("should pass options correctly to the Server", () => { + const engine = new Server({ cors: true }); + expect(engine.opts).to.have.property("cors", true); + }); + }); + + describe("listen", () => { + it("should open a http server that returns 501", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + listen((port) => { + request.get(`http://localhost:${port}`, (err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(501); + done(); + }); + }); + }); + }); + + describe("attach()", () => { + it("should work from require()", () => { + const server = http.createServer(); + const engine = new Server(server); + + expect(engine).to.be.an(Server); + }); + + it("should return an engine.Server", () => { + const server = http.createServer(); + const engine = attach(server); + + expect(engine).to.be.an(Server); + }); + + it("should attach engine to an http server", (done) => { + const server = http.createServer(); + attach(server); + + server.listen(() => { + const uri = `http://localhost:${server.address().port}/engine.io/`; + request.get(uri, (err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(0); + expect(res.body.message).to.be("Transport unknown"); + server.once("close", done); + server.close(); + }); + }); + }); + + it("should destroy upgrades not handled by engine", (done) => { + const server = http.createServer(); + attach(server, { destroyUpgradeTimeout: 50 }); + + server.listen(() => { + const client = net.createConnection(server.address().port); + client.setEncoding("ascii"); + client.write( + [ + "GET / HTTP/1.1", + "Connection: Upgrade", + "Upgrade: IRC/6.9", + "", + "", + ].join("\r\n") + ); + + const check = setTimeout(() => { + done(new Error("Client should have ended")); + }, 100); + + client.on("end", () => { + clearTimeout(check); + done(); + }); + }); + }); + + it("should not destroy unhandled upgrades with destroyUpgrade:false", (done) => { + const server = http.createServer(); + attach(server, { destroyUpgrade: false, destroyUpgradeTimeout: 50 }); + + server.listen(() => { + const client = net.createConnection(server.address().port); + client.on("connect", () => { + client.setEncoding("ascii"); + client.write( + [ + "GET / HTTP/1.1", + "Connection: Upgrade", + "Upgrade: IRC/6.9", + "", + "", + ].join("\r\n") + ); + + setTimeout(() => { + client.removeListener("end", onEnd); + done(); + }, 100); + + function onEnd() { + done(new Error("Client should not end")); + } + + client.on("end", onEnd); + }); + }); + }); + + it("should destroy unhandled upgrades with after a timeout", (done) => { + const server = http.createServer(); + attach(server, { destroyUpgradeTimeout: 200 }); + + server.listen(() => { + const client = net.createConnection(server.address().port); + client.on("connect", () => { + client.setEncoding("ascii"); + client.write( + [ + "GET / HTTP/1.1", + "Connection: Upgrade", + "Upgrade: IRC/6.9", + "", + "", + ].join("\r\n") + ); + + // send from client to server + // tests that socket is still alive + // this will not keep the socket open as the server does not handle it + setTimeout(() => { + client.write("foo"); + }, 100); + + function onEnd() { + done(); + } + + client.on("end", onEnd); + }); + }); + }); + + it("should not destroy handled upgrades with after a timeout", (done) => { + const server = http.createServer(); + attach(server, { destroyUpgradeTimeout: 100 }); + + // write to the socket to keep engine.io from closing it by writing before the timeout + server.on("upgrade", (req, socket) => { + socket.write("foo"); + socket.on("data", (chunk) => { + expect(chunk.toString()).to.be("foo"); + socket.end(); + }); + }); + + server.listen(() => { + const client = net.createConnection(server.address().port); + + client.on("connect", () => { + client.setEncoding("ascii"); + client.write( + [ + "GET / HTTP/1.1", + "Connection: Upgrade", + "Upgrade: IRC/6.9", + "", + "", + ].join("\r\n") + ); + + // test that socket is still open by writing after the timeout period + setTimeout(() => { + client.write("foo"); + }, 200); + + client.on("data", (data) => {}); + + client.on("end", done); + }); + }); + }); + + it("should preserve original request listeners", (done) => { + let listeners = 0; + const server = http.createServer((req, res) => { + expect(req && res).to.be.ok(); + listeners++; + }); + + server.on("request", (req, res) => { + expect(req && res).to.be.ok(); + res.writeHead(200); + res.end(""); + listeners++; + }); + + attach(server); + + server.listen(() => { + const port = server.address().port; + request.get( + `http://localhost:${port}/engine.io/default/`, + (err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(0); + expect(res.body.message).to.be("Transport unknown"); + request.get(`http://localhost:${port}/test`, (err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(listeners).to.eql(2); + server.once("close", done); + server.close(); + }); + } + ); + }); + }); + }); +}); diff --git a/packages/engine.io/test/fixtures/ca.crt b/packages/engine.io/test/fixtures/ca.crt new file mode 100644 index 00000000..9f16dabb --- /dev/null +++ b/packages/engine.io/test/fixtures/ca.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAJKBPV3nMXjsMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTE4MTczODAwWhcNMjUxMTE1MTczODAwWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAmNWKTumE1f+ptArhPTOcaUReoqBlri/ujIzm1N8Qr0hghS6B8eXGngAS +YM7ziTlLZmLKgZg7TYOs+qK+xNNjSMbkA4Tions7vX3FYAokfh1iSiQigAw3TAwg +brUaA0phucJBjvWI2mDuwzTLhQp1wmGrliAJhXag2ZQt817m6wrsFWuwiviMIHlq +mQhC+vwd2SvW4xGf5zxjzCM8m7pOiJCLjxXwvNphiTR3tb807W00mi5cMFwhmAUT +uSiVkVERubIYEKNSW2ynxzGFfb+GF/ddUxCKsnMDfM+SpMrsTBv9BzJzXU7Hc9jP +NPFtrZiVo9aKn8csTSvKifmfiNwl2YGuWlW++1+ew6Q9rqEqvKHnKU+Cuwt3y37U +ryqrBS47cz1xxFb3fCn+a72ytcHjI9lMqIQ0+IZ0/4cf0TK80ECEQ0CyrCk0E9Qz +eMEzIALRa/pI8uTXdoAtQIlOsfALWeni+QphZ1BVjwZRmr+F1Px2/R30+gAcZHKc +D+0Bm6owvpBWDe1s0DrkwtY3fyZ+OPS5/3eQtyhy9/3vnz9WBw0BGZyN2nzs5HsB +RB5qDBRx+NQz1QYp/Ba3WeVmZURe2NMnS4uEypkWahW1XNQ+g+JJhK1p01s0+v/B +f4DodYEcsw/3fRU0AKdsAkabQ68VIJAYyfQyinpNR9sHDKZ6Dx8CAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUdwTc4idMFJo0xYmoLTJQeD7A59kwdQYDVR0jBG4wbIAUdwTc +4idMFJo0xYmoLTJQeD7A59mhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQCS +gT1d5zF47DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQBRLzphOdJO +ekU+iwFLGXJiLfUQi3EORaA3mBtozvyRRUyxu0FqWdGGINcyq7xaZxkQ0P+eBMDv +V3F7FvidhaVe6TR7VJB2gyaWeLd1BSOi5gGBk5kuuqV3VbusNzY+WhJip98HEK+y +XP+Lt/LXJpPwOORF9TiR6IBFQn3fBVhPjsqQRzT468QuZ5DCGQ5UW+wWl43I8OxS +PDiUmHTlwLnLqqVFgSE+VnX4vSUZD8kDf0kkxssg1r56IzneSlRBegSVXIuRCbRf +QmWaxz+D6ffM1eNiE3nQxsgJy3dPL1Lfsaidgz39yAC099pjLyVH23cfmFmT/l5b +OdhRE5D75rL8eXAiI2/voz1M+v7XznHjZEhcVUlFsBXsk3zHb2vQQZRNPLnybTb8 +biFpReSIWdpno+F5IrT7z0L8JI3LU0leEFV+Rf525Q+78Rffohxd51fUj0vLKoy9 +un0IEkOcaJhHTPc2dMXb2qGcV4MaUEUsERrnanGZmdNd1aD3YAG3C+nJ8gxrEGHO +veD6Xbyf1K8e7H2sqhGCm8eyHgCFGQiul6yQ41ZwjKgoSCJvOJaYUFE18k0S9k/I +rWYvYWRYbDj4GYx+6LUTfyo30KK5jl4KAil92LrGlIfWK4IjUJyPlOJkb6gXkj0l +lfbUHTmnKthFwJS0958xBq7UM7+RzyFIOg== +-----END CERTIFICATE----- diff --git a/packages/engine.io/test/fixtures/ca.key b/packages/engine.io/test/fixtures/ca.key new file mode 100644 index 00000000..4568779c --- /dev/null +++ b/packages/engine.io/test/fixtures/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAmNWKTumE1f+ptArhPTOcaUReoqBlri/ujIzm1N8Qr0hghS6B +8eXGngASYM7ziTlLZmLKgZg7TYOs+qK+xNNjSMbkA4Tions7vX3FYAokfh1iSiQi +gAw3TAwgbrUaA0phucJBjvWI2mDuwzTLhQp1wmGrliAJhXag2ZQt817m6wrsFWuw +iviMIHlqmQhC+vwd2SvW4xGf5zxjzCM8m7pOiJCLjxXwvNphiTR3tb807W00mi5c +MFwhmAUTuSiVkVERubIYEKNSW2ynxzGFfb+GF/ddUxCKsnMDfM+SpMrsTBv9BzJz +XU7Hc9jPNPFtrZiVo9aKn8csTSvKifmfiNwl2YGuWlW++1+ew6Q9rqEqvKHnKU+C +uwt3y37UryqrBS47cz1xxFb3fCn+a72ytcHjI9lMqIQ0+IZ0/4cf0TK80ECEQ0Cy +rCk0E9QzeMEzIALRa/pI8uTXdoAtQIlOsfALWeni+QphZ1BVjwZRmr+F1Px2/R30 ++gAcZHKcD+0Bm6owvpBWDe1s0DrkwtY3fyZ+OPS5/3eQtyhy9/3vnz9WBw0BGZyN +2nzs5HsBRB5qDBRx+NQz1QYp/Ba3WeVmZURe2NMnS4uEypkWahW1XNQ+g+JJhK1p +01s0+v/Bf4DodYEcsw/3fRU0AKdsAkabQ68VIJAYyfQyinpNR9sHDKZ6Dx8CAwEA +AQKCAgAg6z3TKXE3Ns4yvXUuXYN/GP7ZQHsmPaTAGUlO6I0LdCd2CEJs+/T/6zwK +JglGsVSQRQ8hQszjMU183rkAZBeqgUxzhZfbL3f6pLByszyQ/XtCRO45bmgqtSH3 +NoLX2pmaDUFZrYFAqEhFO4XqrgoXSDpRJ61lVdvngYc0OGi8j6myI3PvOwHTrNNN +Cv6CWPOE53BtkEpE4DkOqzhOwp5Pw/KLa0pjIxaHGwn916Vqzm7aFso8kFucBtvs +sdUla7TJrpaIXuVKU+j/eqcqIqqbVuh/D70QGr3RkFQhsqOa8RxbBH7cxi8nwLdA +zA+3qHnyxC8voxLjvF7vwRifvetYzOP1YunDU3wraU4sHQn4OXh0TEOhm4QhI2W2 +XSUt9B7zDm2AQIukJPxXoKsCd7D91l8m/suDGlHv3zZoJ6qgLuEZDOThhRq+wCIs +wgzRDgDuQ6CVU1gVnT0FUDj5LZ68qiX9+vA/w3Yky6xSRSTnTvgLaWBsPUBytX4h +eqfo39R1Ztm3UQypx0VyPJIDxVt5pbRMNxb7mqjzGh62fcH4fasl0spt97KKAtJq +3BraN2EP3TeBB4eaHtyZY/aCoOpNqrL0ajEzN9wS2hrS+j8ZIEMdEfADVOGGfnZo +ABl/gRo1m09zAadK5JZlaGx1bZS3ag5ftM+V6S6Ku/LjkINwgQKCAQEAxm4TiJxb +k2taQWwPYaPrkulCjrDbIj1boli8uh4h1JtXvrCQxQ9JFoXJtZmezBkJ7Vz9flr+ +OxR+EGUIc+949bSexwDhVCc1SL5YXVYPu1oeYgOjoVMfh+mzYCgyfK8de8Ijq+gF +Egj77UKsfN5ejG6i1Vs4F+Z+zZzsP95qfE5dPieACzwo0igM8HVZMGavO67T1KhY +oa7e+Jk7Lcw3KL4vHQQK8UAKHwE1/TOgi0KvSQ250hfJBbWUnLFTbHOXelgg1Fpw +sqde/M240Pd2ltKdWxM+awyowiNPkMCHira0RrXdBT0vDbNBy3lvMtm1UpYUoCl0 +MMrpk2G7zeuqDwKCAQEAxSzwbKuK4guIn/FeoLHiMEazhpHub1OY2yBwnE0j44LQ +wIo9G70GTnBaH9gJDj7wOL1xwIdRdNoeWdEQ4wXH/rEH93JrZ1y9jv7LcIegJqQh +C0U4RPE1JC0pic6f1Gw58p0q85Rkkee8oaJfeLZ4eJqy52XJyKRzktiQ1mN5ABYU +gwS7GXee/tcWobWqN2Sq/4TcW/nysxQ5dsKKJPide0zKeNwhxPlahBupwjlsuoa2 +wAsUeXttfY17R/vS0KXbxSzoLII/ClrT2n0OoTxmUK7ht4OuBXgg6ZkjIBQ0ki/n +CZTvClYziLk5NkGR2tw53I/zlXslXKbuTX4ByUSZ8QKCAQEAmLCPe2nF1fSfqQP7 ++ghm989ileZlWT2Zy5047IbPRYibxnKbk+elOB2PD5y8YxVJXEtYDOj8BH5KW1dD +X+MAUyG/pCZ7PYRGLkm6OWhGBsbb5lQij7sk4jLlArMr1mHx8A9934RUkoIzSWkq +zZNXcfyYdFETIuEM5i9AZA1EJ48tlOxUTVDnoH+NJWNHVEVPxj9LZbJ9MT0c+nL+ +5MjmEQX3vv4jZWz/3MfTwZj+iuqvcymKua3v0+LcDo8tQKDaCRzTdlR5sB+2qhWr +h7FEod5Dk5eFSl6dZXZCfYKJSiY5Jsg+4Q8prAMqN+ajuJ9qNbii+nOrovghMHXe +TCBx5QKCAQBas2BpbMO3VbzkbkCsRQeaU3uTxJ9c4KSo8BQ9IhMHPg7O8whHMT2s +aWxbx6HqxrL0NtkTymuDCC77+/r7o5YrJ75VanHTm0qrc7ObsRfPjqKQr6fBtv9O +A+Reuwi0y5AgdYHjiHh20ZXo+GtYeP+T4v23ChC3Vka/3xVJOXrYuk93MX7rqSYf +bku/2XRShOFQJwrC2Ih3Li983OJ1PVQb+ugMjp6OIHIt4RfG+2lzqDJ6xt4FP+zO +231BUKraReGBozWt+8AKAFwB3pMTQliCdt/n7g/n/imNq18IC6NfN9/cfYE0TRDp +rOKPfbwdZD7Nof5X3c0DANsQFI23yvHRAoIBAQCIdbiXKkrgv0fbSNaTpP8XNROV +M+BROigjiQnvAILKCenp1MSKcnIfL24ZfWzRhC5s0WtbsABswK+6pP4lXiXZqvyi +5SJ3/omT13CyNjTDw1LlpSE33FHJKAIpYfP8QVTkOG/8GclR/JUFXicujess2fhw +9F4sUA9txqiyWzhHauU0R4V78jsq1V4VEtGhpapzVNtvpWeCEB33WUiU/EwdLsdz +RnKkvCA0WAFuwhH3bELyFj5sVy8L6kS8QQt6w4T2L/gNkwO01RmCNXSQbYkdYA1Y +9t8FefPnf1Ry86PLdKyg8/LNLS023MpDgt7eCa2/ysnbhDZ5RepZJymcy0rP +-----END RSA PRIVATE KEY----- diff --git a/packages/engine.io/test/fixtures/ca.key.org b/packages/engine.io/test/fixtures/ca.key.org new file mode 100644 index 00000000..185be8da --- /dev/null +++ b/packages/engine.io/test/fixtures/ca.key.org @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,47A0291BAB679B36 + +KuGdIvVLu07u5l1aC/NdXvqLFAbI6NHHosGKs+CuuvnqSd4Yo1XG19SR+irE4vvQ +EWwMypyTy0HN81WEvwx5PFNT+PGokjz6KBJsUwYo6pgS9pXfx9hk/Uo5NKSrH3Qm +3rBVWciPABUiprJaz5PyYZHX/EdzvDmNUpC3HhO0Q4Bfy3DXp8sPvKaoHXYIreKd +SpApbeP3bc2N/mg3qFLPC4tntmo2oYID6PEknrMHgBjvQvRDWHhzISdUTcgYmg2n +p7aQa0HUbixFpDfTKjBn808iYa76AFj5gfG1NaIZ839h1jq1DlT3y4wcNxpEXY8Z +UMvtAtLSEOZYDgJt7kzTJ7tqNYKPluDEoaljLGZeIOYYKDgU6d3Zs67aL036CVSr +YdM3mIl65r196NIYnBeXT0metNMWYEAzuF7xTpaeO5JGDECvKQTYumPWwNjXxO75 +YqWxkmmiJ4TK7ECKfCvefy31wVi1T1+fdGYI3X6F9md/r290URyVfI8AFUkXpJ5c +ubWF82dJ9TwMWfdwPnlxQRlzAh18YuTbzK7MJYE3UOLJuzqU9natueIh0Ek2YOtk +cVuW0VtPoLP7ApfZJoF0LS2YRV6cWDA3XxiSCWhz0m0cvU1xyts44ej5eYaRvScU +Ec3PFliFQrbnKzXwvdZvlhUoEpV+8ldZ6rJTMrZe0WTyQ3r50uLdsc76W/6nwK2/ +lA8tH3zHBjMDgQ3J+KluxajEgU+fyGpucX63BpCs29XBHFvBLkck8YYNO3xvSKpC +7fVu/Vrk89LlQZ9VoX2phidEpgsIji5xK0csXuQ11Zfqu+WCQKO6NS2MUCUFPLmY +ZmWN3BvCo9uSOchfs5vnS3+ismLm2Juzs/gmBgdWVd5MyJtmk8U45Tklo3aVJuow +6I9kZrAE3x9ZEhLgYUgG93DAEGDD2bFfB2HXbfg1FV0mgOqKU3cuh/VUzbtHfiVX +nIoEbue4t76gy2U5gBMfH1hHwM/ONJ9y0LaviWMLX4rEGzgE9nf+ONwykLZqzRdU +1XMPCwfxETR5XnnwvUg1IuYU6PPrQ8KpQyDzq3WNYI4ghUGrXsEJeCX+RXFA8GbS +nlKIYcsbyeRZGipItB9CBB6HX2tL2dyde2jyXgBFl9mk+cLkttT5l0oVFDhp7xUR +b8Z3rFy9XcAbFJ55jjScE+UolKp3jWJUrFmFmHT5ZtLUK3iwtw5jmDCsrWW8mVSV +6daZLwOPc77BYSvlWa5DYSApdfvc4QxMxaXFzvk0q36yPCey0z2RriHTkns+yXVZ +Oxyjj67w8hKh+nLUd6rSVJ1nFPups11btzIMD46VP1+CoieBh23Qr4Jo5tPfHKZ+ +hV6RrQcX0umiEceNPK6xgfUnUecYYbRJNcGhdPLPLdSleITiNmAtBBVKdsD/pdHc +rj7IabZ7/3SAAQv3fYHxfXgmGcCIF3KDuFpRT0BjtdNH9K3XTXUg7xCvCQiCyYBv +UXOsU9F+9VMYe1f+4+5AuAuC8/vhHt3i+h4wGf6feD6/Uq9IN6SWNL6b7Iwwdndl +6/QQDtk13glPlEfxC2m+D9FiqyjFcFwt1dlq/hwmoweJ7PM52Y6+6VJyV2ZUm7NU +OtOeXT53As8tqVKvwH1OE12OFEPoHO3dkbDM8x21uGCaRf+SePK4wWAS4uQJMCUH +CV5BtLUW4CDSZO8TwsLqal3qcL7mlCA7XreFcYiTF6OD0a+b10pZ3NorHwYogkww +3tNr6kFD7LdhCBkS53xcXa7js1jH6LhEaNevFPW5O8I24106LBFmW0blcxpZnCIw +SaqCpy+o3lMQ/Wqpqz3+pKDAArMsR0mPQ2tws0ER/PzzsuOycrrbEouWJVQTI9zl +QTlM09INY/u6uLzLJ35OOU2VXdgpeSvWl1khHGShfZnxa6NjeqNF4Y9lsi876z2g +0iS69qftFGuFgl3YFTZJru/ssfaf4d6cHS+LpPIJiY/q/lFthogJ7rjXKvDK0XR4 +ajnUSplSP8I4Pf45B7KEYaGY6IzQVGgqkcou+tJyWse4Tt5k39Nvahwb6TM3Va2/ +ho1TFFgjWMc/KS/vqdnzFNdBeBHOADoEaFmhYgOzujGu6m8vnMxjH7mWtQHNbNNI +ygwUmnPvfZlPUXvLxFr/OZL+zFZKW2shXWgpt2tdby0tB9Ve7HnBGq2q2OeG7t1U +FEjxMYOW7+bHqkAmW28zIV4vLqdMqr830li6dz0iHM9fBcOL4M0UcqD6qbk4aexD +Up22bATpExoT43voK+JOzZjvAhuTVScasJNGjtkjRG0DpESjOevlgrSIuAg6ygCt +zCXJa0njCBsY12+Mw7kY6rH5ulJXFPBRn+fJaDp0XquKnCVhuktS2M5c3Jjg9LL5 +v7xmRA+Mc0B9OgA6yEHhDtNUYZpW3wk9fUrY2afDXXOT2G4RJc90i/Vh+7NWNGli +gXp2Dd4rbC+M/GAEj6wuycddde6M2dNrvLWce2Eh1IDZZaAEfsJJ0WN+13eUtiZv +8cZMb1/HpUy8hrqUw2wuRTWbG1V9PeaBioMBENCn/Zorlh+l2UHuGECCy9aaxhSH +ufxp67BCF5RopYjf5QFUsYH1M+DbUO5PqryWhD+wGuQ5Eu8n5Xhrva0ny/VO8Csj +2kyFRrgMJStQ66hCj1+cH9rvBqQIrSRcU3zw9iuGYMnAITPvMPNp+hzsY0ttSGlK +xk7ZNDN2XaUipED71H9NkzjrdCIarKCQ2VtTzH9L8DKPcSlPAwSCiKhPVaAwSNZs +k44ZJSAQTLtUFjDQUNQDkEjnrf/xdZglhLfaOAjlvXyZEv4JC89GUccaRfDUHGJL +rbEdfHlD0L2gwHGoRNoYGZA+C161CFlx9lOwNDtOD0nQYhMRHUn69jJIKeJhdK05 +ZxmrGkqBHQR4agctfeVHUcnF7hbqqfKYEGMHc984XnTUAPBAsjFoGYQ65JmhvFva +aDMa8GeMzNMdYNTsJljhYbKlELGMhurJJ7ckkAg6UKQrpUQ7FwmBpU+zaDHKPQ6h +8acak5aJfC/OtIpnPDYTBcC3zLNEOvs2QDtjKSVYK7/6AcD9tiYjo0Q95F1aex+M +uqp0yoL83Oq/KxPnkGMo67ukON66Xt8hrSgVIVzL4PX5Xl1PtLSN4lleNN69S7ic +-----END RSA PRIVATE KEY----- diff --git a/packages/engine.io/test/fixtures/client.crt b/packages/engine.io/test/fixtures/client.crt new file mode 100644 index 00000000..2957b4ab --- /dev/null +++ b/packages/engine.io/test/fixtures/client.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtTCCAZ0CFCt+tjtA9647yZp8eNZurQtNp4x+MA0GCSqGSIb3DQEBDQUAMEUx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIxMTE4MjEyMzU4WhcNMzIxMTE1MjEy +MzU4WjBtMQswCQYDVQQGEwJGSTETMBEGA1UECBMKU29tZS1TdGF0ZTERMA8GA1UE +BxMISGVsc2lua2kxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDET +MBEGA1UEAxMKRm9vIENsaWVudDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA ++ukZ/D/DvjCtd7D8SCBDB1w0WYXkS6hjZTbF82gS1pykpVflsYtTnQoprQIeGQFU +bdSZCvZkBABncQvE0cAzEtNvhFCtR9c74e+xJOGXB1vKeQlRJudxU5jvo4S/uwUG +F1tcov11EQb9sZt0PeSO/PeWoKazBSPB4jrvx3Yv4g0CAwEAATANBgkqhkiG9w0B +AQ0FAAOCAgEAMgC3j2XJ5IPB1raLrnJh8vTncnqMe6OmXpaxk0ZYb42Y66BJKlaE +UpLmLYIiICmuH6R4lc00W5nexPyCT4g+1CUs3PhOJGEwWOBodv6dFJ4ayGWln1aD +QX+W+PRuJAazd7wruVnPxVoEspVO+hcr5byX0F3Auqd9jdQZwFXsWvAo7tZxUnvC +gdjnHt5QgMxqeqzZPTw7dreMsIjN6NrUPWaa26VCvLH0Nv+Jgs+RSVwBKp8tO3e+ +763bi8Htpzt4YfAB7EuRykGlAI42C5ZDzcsq30NpSGgOwveHnlvdl6KhC0QaK71h +QmXwBmEUNX1f+XRnvk+fNb1acfddLLYoPP0zS1BEYOOs7KkyScagsUMsnUSOfv3d ++etklFvaXFD3+b/KwljH3WH1dG4ro3J6GHXX05ncDydDDksYi6aC3wpPZYY7eMFx +RWSxMZHX/bD1YH80a2+jBoskTqz3ZFkkGySMfUcpDCUwQuiwjhLp4sew9RDRB/lv +kJezNSoYgnT44CT+IPoPEL1m5Evkm3C7fVzvnldO3TsWmOoza99xrQ+9gtzlWxgb +Av6jNbnGG1HgDYcvxpRMKWe+6fUAHCcP0PuO+2rcygemNtEKzfMY6Py66w5L9/WW +t0UJWU1rR+kLDS3qLfQqvnbvUMroZ9zxE9CJq6+aKEQEpc79lfiv464= +-----END CERTIFICATE----- diff --git a/packages/engine.io/test/fixtures/client.csr b/packages/engine.io/test/fixtures/client.csr new file mode 100644 index 00000000..ca1ae28e --- /dev/null +++ b/packages/engine.io/test/fixtures/client.csr @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBrTCCARYCAQAwbTELMAkGA1UEBhMCRkkxEzARBgNVBAgTClNvbWUtU3RhdGUx +ETAPBgNVBAcTCEhlbHNpbmtpMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQxEzARBgNVBAMTCkZvbyBDbGllbnQwgZ8wDQYJKoZIhvcNAQEBBQADgY0A +MIGJAoGBAPrpGfw/w74wrXew/EggQwdcNFmF5EuoY2U2xfNoEtacpKVX5bGLU50K +Ka0CHhkBVG3UmQr2ZAQAZ3ELxNHAMxLTb4RQrUfXO+HvsSThlwdbynkJUSbncVOY +76OEv7sFBhdbXKL9dREG/bGbdD3kjvz3lqCmswUjweI678d2L+INAgMBAAGgADAN +BgkqhkiG9w0BAQUFAAOBgQDKGUqjkUxGOisFN70X7ZOW7H99veR9QlixKl5e0W+7 +UtJ+GUtH2WQEb4F72+ruHrdDWQI1VaH9hPOvTRCjlgXiT0RHXpGPbJK/Nc+Eq5dm +kuk/tQeXv6+S1fgYOm0w09rE7pBjQtuAybB55lGZ7k84UE2xTc97Ru14nYFCsZ4z +RA== +-----END CERTIFICATE REQUEST----- diff --git a/packages/engine.io/test/fixtures/client.key b/packages/engine.io/test/fixtures/client.key new file mode 100644 index 00000000..e52bafb5 --- /dev/null +++ b/packages/engine.io/test/fixtures/client.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQD66Rn8P8O+MK13sPxIIEMHXDRZheRLqGNlNsXzaBLWnKSlV+Wx +i1OdCimtAh4ZAVRt1JkK9mQEAGdxC8TRwDMS02+EUK1H1zvh77Ek4ZcHW8p5CVEm +53FTmO+jhL+7BQYXW1yi/XURBv2xm3Q95I7895agprMFI8HiOu/Hdi/iDQIDAQAB +AoGBAOFHTXd4YO1wky82Dy1LGiOPm8kNOC7d33BOv2iN9uwN9J4nzymbqNUE/OpD +TnaxBPcfvNFk6+PT4QxUvsB8ytzDMZ3YC4xyJf5GPP/hfzyWCRjB557WZl1cx7nC +2gA93PBZE7WT1SySXmjsiC7o/2T/0cUaawXOBczHP8oXoEkBAkEA/c1MHs13ojxh +oOj/ibCpYpd2Zv5Hrc5tsh+otDdIrb79IAHnNw7WhMkLs6cLk1MY6jLeCvQtjlUY +H5C/6Ez84QJBAP0VZMgWPw3FVNXPrj833OA6XjyWO+TADpnlrahuDQqWnR3C29Uc +Iq/ApVX2pt2cNIZpiuJ4BYNc44cHjvu6vq0CQQChan1cJc9NhluNLELBfnLsOmpa +bKSH3P8VR19TZsm5fvub7Lnx4WT7xKXFl5scEsCIyts/WjbTDDmwca4r/zLhAkB4 +wkeHbY4CnSDgsKr9AUPEPjWPBURo3vdYmY4mKvTQE5O+iqboZfdrEyoQ/ZMbdRhe +9mdNrmU7DAyI9qNUHAQ1AkAlq/vdkrcq5SRR9uti/1M0/Jaw7l3JutBaW93kdvXx +BX568ezO1PQtXwVSv+uJEkDoST1bkvhqt7hlMu/RkmfG +-----END RSA PRIVATE KEY----- diff --git a/packages/engine.io/test/fixtures/client.key.orig b/packages/engine.io/test/fixtures/client.key.orig new file mode 100644 index 00000000..d9c39ba8 --- /dev/null +++ b/packages/engine.io/test/fixtures/client.key.orig @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,081B123E7DBC9687 + +c8gE0LQMMtV7GhUcP7GHBotlNNAsbyG/gERB//BFlvRPjs43ALvm5xMGUkcsZkw9 +H6ljvvgyRkud06x1mGxqlZ4fLlty0/mdXf6791R9gA/9bKLakHGlwnoVUrEPIeh3 +Iu/3IF0Uz8o3uljSp9eehR8sW5V4o+Z1MszQ+GkcpCyYnDPykyN52QLTM3RcUJ0v +2iJrgxYEAyMA14iIpYM8IsFDYIBvwVfQxyBZnQ+8dDQgVOokz56g+/9emgOsVjWj +bdUKZ7+95RLxCq0hjJd8GnoDn7qtxIPOGVqYcdEwwNyzO43FnpjOFhNDWwsrNrgf +vUHKhZRHB99CkuCvGg4fjVbFd/1/n3eE5VmiQuqoHzPnXH3RDIx2rZSjFo/NkzYx +4XzZ4YOe6vOP7kR18CL+bl32WneMELoh+TPLmsSC3rIP48M8+oQINSZaBrl59Ocw +NW36xgDNSQoia2splVNo51vtZomq1Hb3co6hD43D4xnrc6Aqucm3BsW6UCc/PVKv +HAXLCb+3awIy5NJSYb0qRETE2rKB6LjmKfILtOrto6QYSlkJmQUxqoPXgRAWWNlw +1Ngws84+6UjmGWDBlpZHn0hcO/B7KJAAS/xNSFYDoSu6dAbabxTI/dZCWhw1aN5c +QeYPihCi66F6Vuq/QT89dHtZE4IMPH7R95Yp18tCcyVGxaEiphw3HJYepMxoJesQ +YH8tWQwvD5LaADzNJIKBxMjCOK23GuXWQLJJRf4QWiXaQar69qXULxBT3iqBp/rQ +VKsQByBwJlEo/YSEFjhhMgo0zSbqIRAY1XCDo+dgB2IB1KPAobhSCQ== +-----END RSA PRIVATE KEY----- diff --git a/packages/engine.io/test/fixtures/client.pfx b/packages/engine.io/test/fixtures/client.pfx new file mode 100644 index 00000000..381a0a4f Binary files /dev/null and b/packages/engine.io/test/fixtures/client.pfx differ diff --git a/packages/engine.io/test/fixtures/server-close-upgraded.js b/packages/engine.io/test/fixtures/server-close-upgraded.js new file mode 100644 index 00000000..41f9a26f --- /dev/null +++ b/packages/engine.io/test/fixtures/server-close-upgraded.js @@ -0,0 +1,9 @@ +const { ClientSocket, listen } = require("../common"); + +const engine = listen((port) => { + const socket = new ClientSocket("ws://localhost:" + port); + socket.on("upgrade", () => { + engine.httpServer.close(); + engine.close(); + }); +}); diff --git a/packages/engine.io/test/fixtures/server-close-upgrading.js b/packages/engine.io/test/fixtures/server-close-upgrading.js new file mode 100644 index 00000000..67b32de3 --- /dev/null +++ b/packages/engine.io/test/fixtures/server-close-upgrading.js @@ -0,0 +1,9 @@ +const { ClientSocket, listen } = require("../common"); + +const engine = listen((port) => { + const socket = new ClientSocket("ws://localhost:" + port); + socket.on("upgrading", () => { + engine.httpServer.close(); + engine.close(); + }); +}); diff --git a/packages/engine.io/test/fixtures/server-close.js b/packages/engine.io/test/fixtures/server-close.js new file mode 100644 index 00000000..bfbebd3b --- /dev/null +++ b/packages/engine.io/test/fixtures/server-close.js @@ -0,0 +1,9 @@ +const { ClientSocket, listen } = require("../common"); + +const engine = listen((port) => { + const socket = new ClientSocket("ws://localhost:" + port); + socket.on("open", () => { + engine.httpServer.close(); + engine.close(); + }); +}); diff --git a/packages/engine.io/test/fixtures/server.crt b/packages/engine.io/test/fixtures/server.crt new file mode 100644 index 00000000..acc6c646 --- /dev/null +++ b/packages/engine.io/test/fixtures/server.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDtDCCAZwCFCt+tjtA9647yZp8eNZurQtNp4x/MA0GCSqGSIb3DQEBDQUAMEUx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIxMTE4MjEyNDExWhcNMzIxMTE1MjEy +NDExWjBsMQswCQYDVQQGEwJGSTETMBEGA1UECBMKU29tZS1TdGF0ZTERMA8GA1UE +BxMISGVsc2lua2kxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDES +MBAGA1UEAxMJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA +QLNPDILqMIulvPga6ogci69xYaSp50U7Tv8Hww3m4D9w2ubyGqR7dDW20iGJI7tT +Ncv/UtLqgknEiCBXkq/MDr5gdXNI0V/j+qAOqfJSVxvxxHxLrt7ivjpZHbO7B3K7 +bkWmCxAFzWAlE5U9FXX1b8VfetJv+cDEaXsYwuYfowIDAQABMA0GCSqGSIb3DQEB +DQUAA4ICAQCDSuFPJ++HY5WBhpnumbZ7+T0ReWKaerdNnQ2Xgrna5mZfB2xeRkvY +XeJ9VBCpGgEZKCKkhZCjomn/kLkYzRk4Sqr1ivN5NWl6G/9UTttHdRa3xiR1NhKI +AMYghpel30w5e+cWtsdR06P2FvZMuiMFCyqsbPf1xcEIAXN7HJDswq6g0ppTVZ4L +sXljG/J0vp+jAst4XKGLaGqnt8JaBnpNX9NO2Up3h5j7Pa4Nhm/LZ3Ku5ZVDmS1d +B98Bsgr6tQSSyPNfZW0tGXELsNX1I+wUFw9IXFadRTHkhjeT/GhFw3i12uY7rqzm +uJegTtWDkp1QOajhYhLD9WGXb9teldkAAgZawD6ax/uAzqx/4mBFvsUa3FMcua8k +HF9P2lLzKAcyaKt1cvlfUYmDVZ2Gh+9PgM8SqRpMIqK5jMRvFgemxJXS9BMBrQLp +TCvgRwQZD4mUloRlGNewKfJ0oQ1rY29vwdjTL8+BBS/GR8EuzYnqJG/D2nK0guIN +ze+cSDghA5N2pp/ffnpLWmkIDO+fsGAj3eApLhbPQ1xCXnEv6fOjgUmnxdt41m8d ++pEVBICohnvYgoEERDNAi1onJlBd/eyk0Jn37QiwqhQyrmfgwncvlt2SyzS1IZ7s +cEYreG6QHghBhgYiYo0FMuDCjT6g6Ga+T8nOp0xpZtGEWvHwjLjxvQ== +-----END CERTIFICATE----- diff --git a/packages/engine.io/test/fixtures/server.csr b/packages/engine.io/test/fixtures/server.csr new file mode 100644 index 00000000..9906b700 --- /dev/null +++ b/packages/engine.io/test/fixtures/server.csr @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBrDCCARUCAQAwbDELMAkGA1UEBhMCRkkxEzARBgNVBAgTClNvbWUtU3RhdGUx +ETAPBgNVBAcTCEhlbHNpbmtpMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEAwECzTwyC6jCLpbz4GuqIHIuvcWGkqedFO07/B8MN5uA/cNrm8hqke3Q1 +ttIhiSO7UzXL/1LS6oJJxIggV5KvzA6+YHVzSNFf4/qgDqnyUlcb8cR8S67e4r46 +WR2zuwdyu25FpgsQBc1gJROVPRV19W/FX3rSb/nAxGl7GMLmH6MCAwEAAaAAMA0G +CSqGSIb3DQEBBQUAA4GBAClj/K2DAH5S64T6s7jervmk4N956Ho3aTLBgE+ReXLj +btcTdk3vFbQApAlG6MrSKys4HjpKpP/RENx3Js0HHeb8ELmWtIQNxRhwIpl0K5AD +xorKj+mwngLtVyARb/M7O3E8jYHzBPzpsolKWIY4AavYdmHu+Zhgm4hPKUcW+bAv +-----END CERTIFICATE REQUEST----- diff --git a/packages/engine.io/test/fixtures/server.key b/packages/engine.io/test/fixtures/server.key new file mode 100644 index 00000000..34657862 --- /dev/null +++ b/packages/engine.io/test/fixtures/server.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDAQLNPDILqMIulvPga6ogci69xYaSp50U7Tv8Hww3m4D9w2uby +GqR7dDW20iGJI7tTNcv/UtLqgknEiCBXkq/MDr5gdXNI0V/j+qAOqfJSVxvxxHxL +rt7ivjpZHbO7B3K7bkWmCxAFzWAlE5U9FXX1b8VfetJv+cDEaXsYwuYfowIDAQAB +AoGBAL7tQmXl2fmz/mu5kHhCpKwcuT6TpxEo4aN132aY+qxn1flBHAwiE2mbTmDi +rHViq/2GNrK5UUed3p60RdJSlgwIkyqtcGxWhUJGYCR/hU60qeeLp3MhhOoOFbiV +YTDsoC7V/SuWbX+1qG5FxnHSnTZhAIRkZXS4uTZ5WDcQm/7BAkEA+TlZ1IT9CeU/ +FpHpqc8RgR8377Ehjy8o4Z4EGFnxQlAUWASnhs6dw4isr3+c7hA1OEmqmcRClPVZ +t1JbHAPC4QJBAMV60WSJzPUccCF47T9Ao2CeWFl/9dmgQQe9idpTNuKMXNtPJN44 +0MQvnb+xS828woJOoRI+/UTVLLBc4xwMtwMCQQDZTadExTw4v5l1nX5GoJUbp9PG +/ARN64nSx0u8y9evwVErucs0oL0we+BOGZAEhz9QN/M3pceESDWUwYtNbv4hAkBB +Ku2MqvjK7k6GjTxlgjQn/zkSl+qOnZa4MjEarhlPm5hM+wokl0U1aK07BAwK4b6i +d8YpmkXEAEEWFiEQMZX3AkEA1SkdiFj1u7HnzO7onLJsnFzowX3pm1UFl0azOMlM +2GkjYxWeJ/4VL7Y6QkhHE0Nj3my2+MJQI9xpYgMbw/l11w== +-----END RSA PRIVATE KEY----- diff --git a/packages/engine.io/test/fixtures/server.key.orig b/packages/engine.io/test/fixtures/server.key.orig new file mode 100644 index 00000000..eca29e6d --- /dev/null +++ b/packages/engine.io/test/fixtures/server.key.orig @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,8A14F3F31BF3CDF7 + +84UpLjX5GbfCoP/+ZRLwHZHqmgKyc2cjQOQKhp2YTk+4E0JSO7KsKwJax55r5VqE +ECGvbd8yvVZ3GRpU6bDCLelDN+Ob5ZSGhynI0f9is6Qle1/SbvaK+qL6MLf4H38l +Y91eZxjE8hLvOMHBlbobLmy9UUzl2osBDAM1nm5d1X7pF7mN4xgY8s2G7UPq3ZZS +NWwMsnwtb/9Ahm7WXfsJbebyKspkTSHYp/ozaOGW58fuKxkwRFd1UlyblZWU3ezP +JCpvJQ7HS6Dfy6+GUaxcC6pyxqnoJHYccB5usJ2h4QD4Es3sT7Vw7M80JKw3vAWm +TH7VkFX3yGfJ1p1jNzifN5687QqrjeI3/ecTs1rFhIC4TUPN9EDvw86Y6l6Mvo04 +Hl2cVzCnCrZYq0ICD0op3+7f9kuKl7bz54S/iRG2qQdICohPs7ra2yaUy+NFVDs1 +XdXyF5/xew+Rr2z7ygEd+OrvXxPV0zTFbicg9GXGeB/pIYAclmoSNXD5T3voN7y9 +5MjSGL375N3z+kqPzMNawYCnZLwQ5jYTDUkDTATYpjcIDVDQkzbl8mFp4i+Z9GhL +H8FRAgmBbkgy3dxhFjxr/WzuaTkUCAbGhrtPd8WCPhBXJBzWdrBVR9SE8zT9n8lq +ZwaPwkPLBrLe/XZFEQJ5cdvMVNy4QQwsyxHnCgO3VUc68UYXb39MvyM6t8+S1rSm +SyvVAT+jB8T4VlE7tedQGuvyKMmeNIJe9znd3u4S+Aq2+vw6bOKeNYBMaupR5Gyl +bJrscuTG1aLUe+XH9BuFBUoIwdXuBv4Ko/pDL0MYPghDAEGkp4Acmg== +-----END RSA PRIVATE KEY----- diff --git a/packages/engine.io/test/middlewares.js b/packages/engine.io/test/middlewares.js new file mode 100644 index 00000000..586df621 --- /dev/null +++ b/packages/engine.io/test/middlewares.js @@ -0,0 +1,321 @@ +const listen = require("./common").listen; +const expect = require("expect.js"); +const request = require("superagent"); +const { WebSocket } = require("ws"); +const helmet = require("helmet"); +const session = require("express-session"); +const { ClientSocket } = require("./common"); + +describe("middlewares", () => { + it("should apply middleware (polling)", (done) => { + const engine = listen((port) => { + engine.use((req, res, next) => { + res.setHeader("foo", "bar"); + next(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.eql(200); + expect(res.headers["foo"]).to.eql("bar"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should apply middleware (websocket)", (done) => { + const engine = listen((port) => { + engine.use((req, res, next) => { + res.setHeader("foo", "bar"); + next(); + }); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.on("upgrade", (res) => { + expect(res.headers["foo"]).to.eql("bar"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + + socket.on("open", () => { + socket.close(); + }); + }); + }); + + it("should apply all middlewares in order", (done) => { + const engine = listen((port) => { + let count = 0; + + engine.use((req, res, next) => { + expect(++count).to.eql(1); + next(); + }); + + engine.use((req, res, next) => { + expect(++count).to.eql(2); + next(); + }); + + engine.use((req, res, next) => { + expect(++count).to.eql(3); + next(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.eql(200); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should end the request (polling)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const engine = listen((port) => { + engine.use((req, res, _next) => { + res.writeHead(503); + res.end(); + }); + + engine.on("connection", () => { + done(new Error("should not happen")); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.eql(503); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should end the request (websocket)", (done) => { + const engine = listen((port) => { + engine.use((req, res, _next) => { + res.writeHead(503); + res.end(); + }); + + engine.on("connection", () => { + done(new Error("should not happen")); + }); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.addEventListener("error", () => { + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should work with helmet (polling)", (done) => { + const engine = listen((port) => { + engine.use(helmet()); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.eql(200); + expect(res.headers["x-download-options"]).to.eql("noopen"); + expect(res.headers["x-content-type-options"]).to.eql("nosniff"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should work with helmet (websocket)", (done) => { + const engine = listen((port) => { + engine.use(helmet()); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.on("upgrade", (res) => { + expect(res.headers["x-download-options"]).to.eql("noopen"); + expect(res.headers["x-content-type-options"]).to.eql("nosniff"); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + + socket.on("open", () => { + socket.close(); + }); + }); + }); + + it("should work with express-session (polling)", (done) => { + const engine = listen((port) => { + engine.use( + session({ + secret: "keyboard cat", + resave: false, + saveUninitialized: true, + cookie: {}, + }) + ); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + // expect(res.status).to.eql(200); + expect(res.headers["set-cookie"][0].startsWith("connect.sid=")).to.be( + true + ); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + + it("should work with express-session (websocket)", (done) => { + const engine = listen((port) => { + engine.use( + session({ + secret: "keyboard cat", + resave: false, + saveUninitialized: true, + cookie: {}, + }) + ); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.on("upgrade", (res) => { + expect(res.headers["set-cookie"][0].startsWith("connect.sid=")).to.be( + true + ); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + + socket.on("open", () => { + socket.close(); + }); + }); + }); + + it("should fail on errors (polling)", (done) => { + const engine = listen((port) => { + engine.use((req, res, next) => { + next(new Error("will always fail")); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.eql(400); + + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + + it("should fail on errors (websocket)", (done) => { + const engine = listen((port) => { + engine.use((req, res, next) => { + next(new Error("will always fail")); + }); + + engine.on("connection", () => { + done(new Error("should not connect")); + }); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=websocket` + ); + + socket.addEventListener("error", () => { + if (engine.httpServer) { + engine.httpServer.close(); + } + done(); + }); + }); + }); + }); + + it("should not be receiving data when getting a message longer than maxHttpBufferSize when polling (with a middleware)", (done) => { + const opts = { + allowUpgrades: false, + transports: ["polling"], + maxHttpBufferSize: 5, + }; + const engine = listen(opts, (port) => { + engine.use((req, res, next) => { + next(); + }); + + const socket = new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("message", () => { + done(new Error("Test invalidation (message is longer than allowed)")); + }); + }); + socket.on("open", () => { + socket.send("aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha"); + }); + socket.on("close", (reason) => { + done(); + }); + }); + }); +}); diff --git a/packages/engine.io/test/parser.js b/packages/engine.io/test/parser.js new file mode 100644 index 00000000..723e56c4 --- /dev/null +++ b/packages/engine.io/test/parser.js @@ -0,0 +1,22 @@ +const expect = require("expect.js"); +const parser = require("../build/parser-v3/index.js"); + +describe("parser", () => { + it("properly encodes a mixed payload", (done) => { + parser.encodePayload( + [ + { type: "message", data: "€€€€" }, + { type: "message", data: Buffer.from([1, 2, 3]) }, + ], + true, + (encoded) => { + expect(encoded).to.be.a(Buffer); + + parser.decodePayload(encoded, (decoded) => { + expect(decoded.data).to.eql("€€€€"); + done(); + }); + } + ); + }); +}); diff --git a/packages/engine.io/test/server.js b/packages/engine.io/test/server.js new file mode 100644 index 00000000..129b07c3 --- /dev/null +++ b/packages/engine.io/test/server.js @@ -0,0 +1,3806 @@ +/* eslint-disable standard/no-callback-literal */ + +const http = require("http"); +const https = require("https"); +const fs = require("fs"); +const path = require("path"); +const exec = require("child_process").exec; +const zlib = require("zlib"); +const { Server, Socket, attach } = require(".."); +const { ClientSocket, listen, createPartialDone } = require("./common"); +const expect = require("expect.js"); +const request = require("superagent"); +const cookieMod = require("cookie"); +const { WebSocket } = require("ws"); + +/** + * Tests. + */ + +describe("server", () => { + let engine, client; + + afterEach(() => { + if (engine && engine.httpServer) { + engine.httpServer.close(); + } + if (client) { + client.close(); + } + }); + + describe("verification", () => { + it("should disallow non-existent transports", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(0); + expect(err.message).to.be("Transport unknown"); + expect(err.context.transport).to.be("tobi"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "tobi" }) // no tobi transport - outrageous + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(0); + expect(res.body.message).to.be("Transport unknown"); + partialDone(); + }); + }); + }); + + it("should disallow `constructor` as transports", (done) => { + const partialDone = createPartialDone(done, 2); + + // make sure we check for actual properties - not those present on every {} + engine = listen((port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(0); + expect(err.message).to.be("Transport unknown"); + expect(err.context.transport).to.be("constructor"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io") + .query({ transport: "constructor" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(0); + expect(res.body.message).to.be("Transport unknown"); + partialDone(); + }); + }); + }); + + it("should disallow non-existent sids", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(1); + expect(err.message).to.be("Session ID unknown"); + expect(err.context.sid).to.be("test"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io") + .query({ transport: "polling", sid: "test" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(1); + expect(res.body.message).to.be("Session ID unknown"); + partialDone(); + }); + }); + }); + + it("should disallow requests that are rejected by `allowRequest`", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen( + { + allowRequest: (req, fn) => { + fn("Thou shall not pass", false); + }, + }, + (port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(4); + expect(err.message).to.be("Forbidden"); + expect(err.context.message).to.be("Thou shall not pass"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io") + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(403); + expect(res.body.code).to.be(4); + expect(res.body.message).to.be("Thou shall not pass"); + partialDone(); + }); + } + ); + }); + + it("should disallow connection that are rejected by `allowRequest` (ws)", (done) => { + listen( + { + allowRequest: (req, fn) => { + fn(null, false); + }, + }, + (port) => { + const client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + client.on("error", () => { + done(); + }); + } + ); + }); + + it("should not throw when the client sends invalid data during the handshake (ws only)", (done) => { + listen((port) => { + // will throw "RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear" + request + .get(`http://localhost:${port}/engine.io/`) + .set("connection", "upgrade") + .set("upgrade", "websocket") + .set("Sec-WebSocket-Version", "13") + .set("Sec-WebSocket-Key", "DXR4dX615eRds8nRmlhqtw==") + .query({ transport: "websocket", EIO: 4 }) + .send("test") + .end(() => {}); + + setTimeout(done, 50); + }); + }); + + it("should not throw when the client sends invalid data during the handshake (upgrade)", (done) => { + listen((port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", EIO: 4 }) + .end((err, res) => { + const sid = JSON.parse(res.text.slice(1)).sid; + + request + .get(`http://localhost:${port}/engine.io/`) + .set("connection", "upgrade") + .set("upgrade", "websocket") + .set("Sec-WebSocket-Version", "13") + .set("Sec-WebSocket-Key", "DXR4dX615eRds8nRmlhqtw==") + .query({ transport: "websocket", EIO: 4, sid }) + .send("test") + .end(() => {}); + + setTimeout(done, 50); + }); + }); + }); + + it("should disallow `__proto__` as transport (polling)", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(0); + expect(err.message).to.be("Transport unknown"); + expect(err.context.transport).to.be("__proto__"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "__proto__", EIO: 4 }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(0); + expect(res.body.message).to.be("Transport unknown"); + partialDone(); + }); + }); + }); + + it("should disallow `__proto__` as transport (websocket)", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(0); + expect(err.message).to.be("Transport unknown"); + expect(err.context.transport).to.be("__proto__"); + partialDone(); + }); + + const socket = new WebSocket( + `ws://localhost:${port}/engine.io/?EIO=4&transport=__proto__` + ); + + socket.onerror = partialDone; + }); + }); + }); + + describe("handshake", () => { + it("should send the io cookie", (done) => { + listen({ cookie: true }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", EIO: 4 }) + .end((err, res) => { + expect(err).to.be(null); + // hack-obtain sid + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; Path=/; HttpOnly; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should send the io cookie custom name", (done) => { + listen({ cookie: { name: "woot" } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `woot=${sid}; Path=/; HttpOnly; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should send the cookie with custom path", (done) => { + listen({ cookie: { path: "/custom" } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; Path=/custom; HttpOnly; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should send the cookie with path=false", (done) => { + listen({ cookie: { path: false } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should send the io cookie with httpOnly=true", (done) => { + listen({ cookie: { httpOnly: true } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; Path=/; HttpOnly; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should send the io cookie with sameSite=strict", (done) => { + listen({ cookie: { sameSite: "strict" } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; Path=/; HttpOnly; SameSite=Strict` + ); + done(); + }); + }); + }); + + it("should send the io cookie with httpOnly=false", (done) => { + listen({ cookie: { httpOnly: false } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; Path=/; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should send the io cookie with httpOnly not boolean", (done) => { + listen({ cookie: { httpOnly: "no" } }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", b64: 1 }) + .end((err, res) => { + expect(err).to.be(null); + const sid = res.text.match(/"sid":"([^"]+)"/)[1]; + expect(res.headers["set-cookie"][0]).to.be( + `io=${sid}; Path=/; HttpOnly; SameSite=Lax` + ); + done(); + }); + }); + }); + + it("should not send the io cookie", (done) => { + listen({ cookie: false }, (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.headers["set-cookie"]).to.be(undefined); + done(); + }); + }); + }); + + it("should register a new client", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + expect(Object.keys(engine.clients)).to.have.length(0); + expect(engine.clientsCount).to.be(0); + + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + expect(Object.keys(engine.clients)).to.have.length(1); + expect(engine.clientsCount).to.be(1); + done(); + }); + }); + }); + + it("should register a new client with custom id", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + expect(Object.keys(engine.clients)).to.have.length(0); + expect(engine.clientsCount).to.be(0); + + const customId = "CustomId" + Date.now(); + + engine.generateId = (req) => customId; + + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.once("open", () => { + expect(Object.keys(engine.clients)).to.have.length(1); + expect(engine.clientsCount).to.be(1); + expect(socket.id).to.be(customId); + expect(engine.clients[customId].id).to.be(customId); + done(); + }); + }); + }); + + it("should register a new client with custom id (with a Promise)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const customId = "CustomId" + Date.now(); + + engine.generateId = () => Promise.resolve(customId); + + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.once("open", () => { + expect(socket.id).to.be(customId); + expect(engine.clients[customId].id).to.be(customId); + done(); + }); + }); + }); + + it("should disallow connection that are rejected by `generateId`", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen({ allowUpgrades: false }, (port) => { + engine.generateId = () => { + return Promise.reject(new Error("nope")); + }; + + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(3); + expect(err.message).to.be("Bad request"); + expect(err.context.name).to.be("ID_GENERATION_ERROR"); + partialDone(); + }); + + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("error", () => { + partialDone(); + }); + }); + }); + + it("should disallow connection that are rejected by `generateId` (websocket only)", function (done) { + if (process.env.EIO_WS_ENGINE === "eiows") { + return this.skip(); + } + const partialDone = createPartialDone(done, 2); + + engine = listen({ allowUpgrades: false }, (port) => { + engine.generateId = () => { + return Promise.reject(new Error("nope")); + }; + + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(3); + expect(err.message).to.be("Bad request"); + expect(err.context.name).to.be("ID_GENERATION_ERROR"); + partialDone(); + }); + + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.on("error", () => { + partialDone(); + }); + }); + }); + + it("should exchange handshake data", (done) => { + listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("handshake", (obj) => { + expect(obj.sid).to.be.a("string"); + expect(obj.pingTimeout).to.be.a("number"); + expect(obj.upgrades).to.be.an("array"); + expect(obj.maxPayload).to.eql(1000000); + done(); + }); + }); + }); + + it("should allow custom ping timeouts", (done) => { + listen({ allowUpgrades: false, pingTimeout: 123 }, (port) => { + const socket = new ClientSocket(`http://localhost:${port}`); + socket.on("handshake", (obj) => { + expect(obj.pingTimeout).to.be(123); + done(); + }); + }); + }); + + it("should trigger a connection event with a Socket", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (socket) => { + expect(socket).to.be.an(Socket); + done(); + }); + }); + }); + + it("should open with polling by default", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (socket) => { + expect(socket.transport.name).to.be("polling"); + done(); + }); + }); + }); + + it("should be able to open with ws directly", (done) => { + const engine = listen({ transports: ["websocket"] }, (port) => { + new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + engine.on("connection", (socket) => { + expect(socket.transport.name).to.be("websocket"); + done(); + }); + }); + }); + + it("should not suggest any upgrades for websocket", (done) => { + listen({ transports: ["websocket"] }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.on("handshake", (obj) => { + expect(obj.upgrades).to.have.length(0); + done(); + }); + }); + }); + + it("should not suggest upgrades when none are availble", (done) => { + listen({ transports: ["polling"] }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, {}); + socket.on("handshake", (obj) => { + expect(obj.upgrades).to.have.length(0); + done(); + }); + }); + }); + + it("should only suggest available upgrades", (done) => { + listen({ transports: ["polling"] }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, {}); + socket.on("handshake", (obj) => { + expect(obj.upgrades).to.have.length(0); + done(); + }); + }); + }); + + it("should suggest all upgrades when no transports are disabled", (done) => { + listen({}, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, {}); + socket.on("handshake", (obj) => { + expect(obj.upgrades).to.have.length(1); + expect(obj.upgrades).to.have.contain("websocket"); + done(); + }); + }); + }); + + it("default to polling when proxy doesn't support websocket", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen({ allowUpgrades: false }, (port) => { + engine.on("connection", (socket) => { + socket.on("message", (msg) => { + if ("echo" === msg) socket.send(msg); + }); + }); + + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(3); + expect(err.message).to.be("Bad request"); + expect(err.context.name).to.be("TRANSPORT_MISMATCH"); + expect(err.context.transport).to.be("websocket"); + expect(err.context.previousTransport).to.be("polling"); + partialDone(); + }); + + var socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + request + .get(`http://localhost:${port}/engine.io/`) + .set({ connection: "close" }) + .query({ transport: "websocket", sid: socket.id }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(3); + socket.send("echo"); + socket.on("message", (msg) => { + expect(msg).to.be("echo"); + partialDone(); + }); + }); + }); + }); + }); + + it("should allow arbitrary data through query string", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + new ClientSocket(`ws://localhost:${port}`, { query: { a: "b" } }); + engine.on("connection", (conn) => { + expect(conn.request._query).to.have.keys("transport", "a"); + expect(conn.request._query.a).to.be("b"); + done(); + }); + }); + }); + + it("should allow data through query string in uri", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + new ClientSocket(`ws://localhost:${port}?a=b&c=d`); + engine.on("connection", (conn) => { + expect(conn.request._query.EIO).to.be.a("string"); + expect(conn.request._query.a).to.be("b"); + expect(conn.request._query.c).to.be("d"); + done(); + }); + }); + }); + + it("should disallow bad requests (handshake error)", function (done) { + const partialDone = createPartialDone(done, 2); + + engine = listen( + { + cors: { credentials: true, origin: "http://engine.io" }, + }, + (port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(3); + expect(err.message).to.be("Bad request"); + expect(err.context.name).to.be("TRANSPORT_HANDSHAKE_ERROR"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io") + .query({ transport: "websocket" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(3); + expect(res.body.message).to.be("Bad request"); + expect(res.header["access-control-allow-credentials"]).to.be( + "true" + ); + expect(res.header["access-control-allow-origin"]).to.be( + "http://engine.io" + ); + partialDone(); + }); + } + ); + }); + + it("should disallow invalid origin header", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + // we can't send an invalid header through request.get + // so add an invalid char here + engine.prepare = function (req) { + Server.prototype.prepare.call(engine, req); + req.headers.origin += "\n"; + }; + + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(3); + expect(err.message).to.be("Bad request"); + expect(err.context.name).to.be("INVALID_ORIGIN"); + expect(err.context.origin).to.be("http://engine.io/\n"); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io/") + .query({ transport: "websocket" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(3); + expect(res.body.message).to.be("Bad request"); + partialDone(); + }); + }); + }); + + it("should disallow invalid handshake method", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(2); + expect(err.message).to.be("Bad handshake method"); + expect(err.context.method).to.be("OPTIONS"); + partialDone(); + }); + + request + .options(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(2); + expect(res.body.message).to.be("Bad handshake method"); + partialDone(); + }); + }); + }); + + it("should disallow unsupported protocol versions", (done) => { + const partialDone = createPartialDone(done, 2); + + const httpServer = http.createServer(); + const engine = new Server({ allowEIO3: false }); + engine.attach(httpServer); + httpServer.listen(() => { + const port = httpServer.address().port; + + engine.on("connection_error", (err) => { + expect(err.req).to.be.ok(); + expect(err.code).to.be(5); + expect(err.message).to.be("Unsupported protocol version"); + expect(err.context.protocol).to.be(3); + + httpServer.close(); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", EIO: 3 }) + .end((err, res) => { + expect(err).to.be.an(Error); + expect(res.status).to.be(400); + expect(res.body.code).to.be(5); + expect(res.body.message).to.be("Unsupported protocol version"); + partialDone(); + }); + }); + }); + + it("should send a packet along with the handshake", (done) => { + listen({ initialPacket: "faster!" }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be("faster!"); + done(); + }); + }); + }); + }); + + it("should support requests without trailing slash", (done) => { + listen({ addTrailingSlash: false }, (port) => { + const partialDone = createPartialDone(done, 2); + + request + .get(`http://localhost:${port}/engine.io`) + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/foo/bar/`) + .query({ transport: "polling" }) + .end((err, res) => { + if (process.env.EIO_WS_ENGINE === "uws") { + expect(err).to.not.be(null); + expect(err.message).to.be("socket hang up"); + } else { + expect(err).to.be(null); + // this should not work, but it is kept for backward-compatibility + expect(res.status).to.be(200); + } + partialDone(); + }); + }); + }); + }); + + describe("close", () => { + it("should be able to access non-empty writeBuffer at closing (server)", (done) => { + const opts = { allowUpgrades: false }; + const engine = listen(opts, (port) => { + new ClientSocket(`http://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(conn.writeBuffer.length).to.be(1); + setTimeout(() => { + expect(conn.writeBuffer.length).to.be(0); // writeBuffer has been cleared + }, 10); + done(); + }); + conn.writeBuffer.push({ type: "message", data: "foo" }); + conn.onError(""); + }); + }); + }); + + it("should be able to access non-empty writeBuffer at closing (client)", (done) => { + const opts = { allowUpgrades: false }; + listen(opts, (port) => { + const socket = new ClientSocket(`http://localhost:${port}`); + socket.on("open", () => { + socket.on("close", (reason) => { + expect(socket.writeBuffer.length).to.be(1); + setTimeout(() => { + expect(socket.writeBuffer.length).to.be(0); + }, 10); + done(); + }); + socket.writeBuffer.push({ type: "message", data: "foo" }); + socket.onError(""); + }); + }); + }); + + it("should trigger on server if the client does not pong", (done) => { + const opts = { allowUpgrades: false, pingInterval: 5, pingTimeout: 5 }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`http://localhost:${port}`); + socket.sendPacket = () => {}; + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(reason).to.be("ping timeout"); + done(); + }); + }); + }); + }); + + it("should trigger on server even when there is no outstanding polling request (GH-198)", (done) => { + const opts = { + allowUpgrades: false, + pingInterval: 500, + pingTimeout: 500, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`http://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(reason).to.be("ping timeout"); + done(); + }); + // client abruptly disconnects, no polling request on this tick since we've just connected + socket.sendPacket = () => {}; + socket.transport.removeListener("packet"); + socket.close(); + // then server app tries to close the socket, since client disappeared + conn.close(); + }); + }); + }); + + it("should trigger on client if server does not meet ping timeout", (done) => { + const opts = { allowUpgrades: false, pingInterval: 50, pingTimeout: 30 }; + listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + // override onPacket and Transport#onClose to simulate an inactive server after handshake + socket.transport.removeListener("packet"); + socket.transport.removeListener("close"); + socket.on("close", (reason, err) => { + expect(reason).to.be("ping timeout"); + done(); + }); + }); + }); + }); + + it("should trigger on both ends upon ping timeout", (done) => { + const opts = { allowUpgrades: false, pingTimeout: 50, pingInterval: 50 }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let total = 2; + + function onClose(reason, err) { + expect(reason).to.be("ping timeout"); + --total || done(); + } + + engine.on("connection", (conn) => { + conn.on("close", onClose); + }); + + socket.on("open", () => { + // override onPacket and Transport#onClose to simulate an inactive server after handshake + socket.sendPacket = () => {}; + socket.transport.removeListener("packet"); + socket.transport.removeListener("close"); + socket.on("close", onClose); + }); + }); + }); + + it("should trigger when server closes a client", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let total = 2; + + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(reason).to.be("forced close"); + --total || done(); + }); + setTimeout(() => { + conn.close(); + }, 10); + }); + + socket.on("open", () => { + socket.on("close", (reason) => { + expect(reason).to.be("transport close"); + --total || done(); + }); + }); + }); + }); + + it("should trigger when server closes a client (ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let total = 2; + + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(reason).to.be("forced close"); + --total || done(); + }); + setTimeout(() => { + conn.close(); + }, 10); + }); + + socket.on("open", () => { + socket.on("close", (reason) => { + expect(reason).to.be("transport close"); + --total || done(); + }); + }); + }); + }); + + it("should allow client reconnect after restarting (ws)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const opts = { transports: ["websocket"] }; + const engine = listen(opts, (port) => { + engine.httpServer.close(); + engine.httpServer.listen(port); + + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.once("connection", (conn) => { + setTimeout(() => { + conn.close(); + }, 10); + }); + + socket.once("close", (reason) => { + expect(reason).to.be("transport close"); + done(); + }); + }); + }); + + it("should trigger when client closes", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let total = 2; + + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(reason).to.be("transport close"); + --total || done(); + }); + }); + + socket.on("open", () => { + socket.on("close", (reason) => { + expect(reason).to.be("forced close"); + --total || done(); + }); + + setTimeout(() => { + socket.close(); + }, 10); + }); + }); + }); + + it("should trigger when client closes (ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let total = 2; + + engine.on("connection", (conn) => { + conn.on("close", (reason) => { + expect(reason).to.be("transport close"); + --total || done(); + }); + }); + + socket.on("open", () => { + socket.on("close", (reason) => { + expect(reason).to.be("forced close"); + --total || done(); + }); + + setTimeout(() => { + socket.close(); + }, 10); + }); + }); + }); + + it("should trigger when calling socket.close() in payload", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + + engine.on("connection", (conn) => { + conn.send(null, () => { + socket.close(); + }); + conn.send("this should not be handled"); + + conn.on("close", (reason) => { + expect(reason).to.be("transport close"); + done(); + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.not.be("this should not be handled"); + }); + + socket.on("close", (reason) => { + expect(reason).to.be("forced close"); + }); + }); + }); + }); + + it("should abort upgrade if socket is closed (GH-35)", (done) => { + listen({ allowUpgrades: true }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + socket.close(); + // we wait until complete to see if we get an uncaught EPIPE + setTimeout(() => { + done(); + }, 100); + }); + }); + }); + + it("should abort connection when upgrade fails", (done) => { + listen({ allowUpgrades: true }, (port) => { + const req = http.request( + { + port, + path: "/engine.io/", + headers: { + connection: "Upgrade", + upgrade: "websocket", + }, + }, + (res) => { + expect(res.statusCode).to.eql(400); + res.resume(); + res.on("end", done); + } + ); + req.end(); + }); + }); + + it( + "should trigger if a poll request is ongoing and the underlying " + + "socket closes, as in a browser tab close", + ($done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + // hack to access the sockets created by node-xmlhttprequest + // see: https://github.com/driverdan/node-XMLHttpRequest/issues/44 + const request = require("http").request; + const sockets = []; + http.request = function (opts) { + const req = request.apply(null, arguments); + req.on("socket", (socket) => { + sockets.push(socket); + }); + return req; + }; + + function done() { + http.request = request; + $done(); + } + + var socket = new ClientSocket(`ws://localhost:${port}`); + let serverSocket; + + engine.on("connection", (s) => { + serverSocket = s; + }); + + socket.transport.on("poll", () => { + // we set a timer to wait for the request to actually reach + setTimeout(() => { + // at this time server's `connection` should have been fired + expect(serverSocket).to.be.an("object"); + + // OPENED readyState is expected - we are actually polling + expect(socket.transport.pollXhr.xhr.readyState).to.be(1); + + // 2 requests sent to the server over an unique port means + // we should have been assigned 2 sockets + expect(sockets.length).to.be(2); + + // expect the socket to be open at this point + expect(serverSocket.readyState).to.be("open"); + + // kill the underlying connection + sockets[1].end(); + serverSocket.on("close", (reason, err) => { + expect(reason).to.be("transport error"); + expect(err.message).to.be("poll connection closed prematurely"); + done(); + }); + }, 50); + }); + }); + } + ); + + it("should not trigger with connection: close header", ($done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + // intercept requests to add connection: close + const request = http.request; + http.request = function () { + const opts = arguments[0]; + opts.headers = opts.headers || {}; + opts.headers.Connection = "close"; + return request.apply(this, arguments); + }; + + function done() { + http.request = request; + $done(); + } + + engine.on("connection", (socket) => { + socket.on("message", (msg) => { + expect(msg).to.equal("test"); + socket.send("woot"); + }); + }); + + var socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + socket.send("test"); + }); + socket.on("message", (msg) => { + expect(msg).to.be("woot"); + done(); + }); + }); + }); + + it( + "should not trigger early with connection `ping timeout`" + + "after post handshake timeout", + (done) => { + // first timeout should trigger after `pingInterval + pingTimeout`, + // not just `pingTimeout`. + const opts = { + allowUpgrades: false, + pingInterval: 300, + pingTimeout: 100, + }; + listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let clientCloseReason = null; + + socket.on("handshake", () => { + socket.transport.removeListener("packet"); + }); + socket.on("open", () => { + socket.on("close", (reason) => { + clientCloseReason = reason; + }); + }); + + setTimeout(() => { + expect(clientCloseReason).to.be(null); + done(); + }, 200); + }); + } + ); + + it( + "should not trigger early with connection `ping timeout` " + + "after post ping timeout", + (done) => { + // ping timeout should trigger after `pingInterval + pingTimeout`, + // not just `pingTimeout`. + const opts = { + allowUpgrades: false, + pingInterval: 80, + pingTimeout: 50, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let clientCloseReason = null; + + engine.on("connection", (conn) => { + conn.on("heartbeat", () => { + conn.onPacket = () => {}; + }); + }); + + socket.on("open", () => { + socket.on("close", (reason) => { + clientCloseReason = reason; + }); + }); + + setTimeout(() => { + expect(clientCloseReason).to.be(null); + done(); + }, 100); + }); + } + ); + + it( + "should trigger early with connection `transport close` " + + "after missing pong", + (done) => { + // ping timeout should trigger after `pingInterval + pingTimeout`, + // not just `pingTimeout`. + const opts = { + allowUpgrades: false, + pingInterval: 80, + pingTimeout: 50, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let clientCloseReason = null; + + socket.on("open", () => { + socket.on("close", (reason) => { + clientCloseReason = reason; + }); + }); + + engine.on("connection", (conn) => { + conn.on("heartbeat", () => { + setTimeout(() => { + conn.close(); + }, 20); + setTimeout(() => { + expect(clientCloseReason).to.be("transport close"); + done(); + }, 100); + }); + }); + }); + } + ); + + if (process.env.EIO_CLIENT === "3") { + it( + "should trigger with connection `ping timeout` " + + "after `pingInterval + pingTimeout`", + (done) => { + const opts = { + allowUpgrades: false, + pingInterval: 300, + pingTimeout: 100, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let clientCloseReason = null; + + socket.on("open", () => { + socket.on("close", (reason) => { + clientCloseReason = reason; + }); + }); + + engine.on("connection", (conn) => { + conn.once("heartbeat", () => { + setTimeout(() => { + socket.transport.removeListener("packet"); + expect(clientCloseReason).to.be(null); + }, 150); + setTimeout(() => { + expect(clientCloseReason).to.be(null); + }, 350); + setTimeout(() => { + expect(clientCloseReason).to.be("ping timeout"); + done(); + }, 500); + }); + }); + }); + } + ); + } else { + it( + "should trigger with connection `ping timeout` " + + "after `pingInterval + pingTimeout`", + (done) => { + const opts = { + allowUpgrades: false, + pingInterval: 300, + pingTimeout: 100, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let clientCloseReason = null; + + socket.on("open", () => { + socket.on("close", (reason) => { + clientCloseReason = reason; + }); + }); + + engine.on("connection", (conn) => { + conn.once("heartbeat", () => { + socket.transport.removeListener("packet"); + setTimeout(() => { + expect(clientCloseReason).to.be(null); + }, 150); + setTimeout(() => { + expect(clientCloseReason).to.be(null); + }, 350); + setTimeout(() => { + expect(clientCloseReason).to.be("ping timeout"); + done(); + }, 500); + }); + }); + }); + } + ); + } + + it( + "should abort the polling data request if it is " + "in progress", + function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const engine = listen({ transports: ["polling"] }, (port) => { + const socket = new ClientSocket(`http://localhost:${port}`); + + engine.on("connection", (conn) => { + const onDataRequest = conn.transport.onDataRequest; + conn.transport.onDataRequest = (req, res) => { + engine.httpServer.close(done); + onDataRequest.call(conn.transport, req, res); + req.removeAllListeners(); + conn.close(); + }; + }); + + socket.on("open", () => { + socket.send("test"); + }); + }); + } + ); + + // tests https://github.com/LearnBoost/engine.io-client/issues/207 + // websocket test, transport error + it("should trigger transport close before open for ws", (done) => { + const opts = { transports: ["websocket"] }; + listen(opts, (port) => { + const url = `ws://0.0.0.0:${port}`; + const socket = new ClientSocket(url); + socket.on("open", () => { + done(new Error("Test invalidation")); + }); + socket.on("close", (reason) => { + expect(reason).to.be("transport error"); + done(); + }); + }); + }); + + // tests https://github.com/LearnBoost/engine.io-client/issues/207 + // polling test, transport error + it("should trigger transport close before open for xhr", (done) => { + const opts = { transports: ["polling"] }; + listen(opts, (port) => { + const socket = new ClientSocket(`http://invalidserver:${port}`); + socket.on("open", () => { + done(new Error("Test invalidation")); + }); + socket.on("close", (reason) => { + expect(reason).to.be("transport error"); + done(); + }); + }); + }); + + // tests https://github.com/LearnBoost/engine.io-client/issues/207 + // websocket test, force close + it("should trigger force close before open for ws", (done) => { + const opts = { transports: ["websocket"] }; + listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + done(new Error("Test invalidation")); + }); + socket.on("close", (reason) => { + expect(reason).to.be("forced close"); + done(); + }); + socket.close(); + }); + }); + + // tests https://github.com/LearnBoost/engine.io-client/issues/207 + // polling test, force close + it("should trigger force close before open for xhr", (done) => { + const opts = { transports: ["polling"] }; + listen(opts, (port) => { + const socket = new ClientSocket(`http://localhost:${port}`); + socket.on("open", () => { + done(new Error("Test invalidation")); + }); + socket.on("close", (reason) => { + expect(reason).to.be("forced close"); + done(); + }); + socket.close(); + }); + }); + + it("should close transport upon ping timeout (ws)", (done) => { + const opts = { + allowUpgrades: false, + transports: ["websocket"], + pingInterval: 50, + pingTimeout: 30, + }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + conn.transport.on("close", done); + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + // override to simulate an inactive client + socket.sendPacket = socket.onHeartbeat = () => {}; + }); + }); + + it("should close transport upon ping timeout (polling)", (done) => { + const opts = { + allowUpgrades: false, + transports: ["polling"], + pingInterval: 50, + pingTimeout: 30, + }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + conn.transport.on("close", done); + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + // override to simulate an inactive client + socket.sendPacket = socket.onHeartbeat = () => {}; + }); + }); + + it("should close transport upon parse error (ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + conn.transport.on("close", done); + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.on("open", () => { + socket.transport.ws.send("invalid"); + }); + }); + }); + + it("should close transport upon parse error (polling)", (done) => { + const opts = { allowUpgrades: false, transports: ["polling"] }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + conn.transport.closeTimeout = 100; + conn.transport.on("close", done); + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + socket.on("open", () => { + socket.transport.doWrite("invalid", () => {}); + }); + }); + }); + + it("should close upgrading transport upon socket close", (done) => { + const engine = listen((port) => { + engine.on("connection", (conn) => { + conn.on("upgrading", (transport) => { + transport.on("close", done); + conn.close(); + }); + }); + new ClientSocket(`ws://localhost:${port}`); + }); + }); + + it("should close upgrading transport upon upgrade timeout", (done) => { + const opts = { upgradeTimeout: 100 }; + const engine = listen(opts, (port) => { + engine.on("connection", (conn) => { + conn.on("upgrading", (transport) => { + transport.on("close", done); + }); + }); + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("upgrading", (transport) => { + // override not to complete upgrading + transport.send = () => {}; + }); + }); + }); + + it("should not timeout after an upgrade", (done) => { + const opts = { pingInterval: 200, pingTimeout: 20 }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + setTimeout(() => { + socket.removeListener("close"); + engine.close(); + socket.close(); + done(); + }, 500); + }); + socket.on("close", () => { + done(new Error("should not happen")); + }); + }); + }); + + it("should not crash when messing with Object prototype", (done) => { + Object.prototype.foo = "bar"; // eslint-disable-line no-extend-native + const engine = listen({ allowUpgrades: true }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + engine.close(); + setTimeout(() => { + delete Object.prototype.foo; + done(); + }, 100); + }); + }); + }); + + describe("graceful close", () => { + before(function () { + if (process.env.EIO_WS_ENGINE === "uws") { + this.skip(); + } + }); + + function fixture(filename) { + return ( + process.execPath + " " + path.join(__dirname, "fixtures", filename) + ); + } + + it("should stop socket and timers", (done) => { + exec(fixture("server-close.js"), done); + }); + + it("should stop upgraded socket and timers", (done) => { + exec(fixture("server-close-upgraded.js"), done); + }); + + it("should stop upgrading socket and timers", (done) => { + exec(fixture("server-close-upgrading.js"), done); + }); + }); + }); + + describe("messages", function () { + this.timeout(5000); + + it("should arrive from server to client", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.send("a"); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be("a"); + done(); + }); + }); + }); + }); + + it("should arrive from server to client (multiple)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + const expected = ["a", "b", "c"]; + let i = 0; + + engine.on("connection", (conn) => { + conn.send("a"); + // we use set timeouts to ensure the messages are delivered as part + // of different. + setTimeout(() => { + conn.send("b"); + + setTimeout(() => { + // here we make sure we buffer both the close packet and + // a regular packet + conn.send("c"); + conn.close(); + }, 50); + }, 50); + + conn.on("close", () => { + // since close fires right after the buffer is drained + setTimeout(() => { + expect(i).to.be(3); + done(); + }, 50); + }); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be(expected[i++]); + }); + }); + }); + }); + + it("should not be receiving data when getting a message longer than maxHttpBufferSize when polling", (done) => { + const opts = { + allowUpgrades: false, + transports: ["polling"], + maxHttpBufferSize: 5, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + done( + new Error("Test invalidation (message is longer than allowed)") + ); + }); + }); + socket.on("open", () => { + socket.send("aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha"); + }); + socket.on("close", () => { + done(); + }); + }); + }); + + it("should not be receiving data when getting a message longer than maxHttpBufferSize (websocket)", (done) => { + const opts = { maxHttpBufferSize: 5 }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + done( + new Error("Test invalidation (message is longer than allowed)") + ); + }); + }); + socket.on("open", () => { + socket.send("aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha"); + }); + socket.on("close", () => { + done(); + }); + }); + }); + + it("should receive data when getting a message shorter than maxHttpBufferSize when polling", (done) => { + const opts = { + allowUpgrades: false, + transports: ["polling"], + maxHttpBufferSize: 5, + }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + expect(msg).to.be("a"); + done(); + }); + }); + socket.on("open", () => { + socket.send("a"); + }); + }); + }); + + it("should arrive from server to client (ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + engine.on("connection", (conn) => { + conn.send("a"); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be("a"); + done(); + }); + }); + }); + }); + + it("should arrive from server to client (multiple, ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + const expected = ["a", "b", "c"]; + let i = 0; + + engine.on("connection", (conn) => { + conn.send("a"); + setTimeout(() => { + conn.send("b"); + setTimeout(() => { + conn.send("c"); + conn.close(); + }, 50); + }, 50); + conn.on("close", () => { + setTimeout(() => { + expect(i).to.be(3); + done(); + }, 50); + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be(expected[i++]); + }); + }); + }); + }); + + it("should arrive from server to client (multiple, no delay, ws)", (done) => { + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + const expected = ["a", "b", "c"]; + let i = 0; + + engine.on("connection", (conn) => { + conn.on("close", () => { + setTimeout(() => { + expect(i).to.be(3); + done(); + }, 50); + }); + conn.send("a"); + conn.send("b"); + conn.send("c"); + conn.close(); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be(expected[i++]); + }); + }); + }); + }); + + it("should arrive when binary data is sent as Int8Array (ws)", (done) => { + const binaryData = new Int8Array(5); + for (let i = 0; i < binaryData.length; i++) { + binaryData[i] = i; + } + + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.on("connection", (conn) => { + conn.send(binaryData); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + for (let i = 0; i < binaryData.length; i++) { + const num = msg.readInt8(i); + expect(num).to.be(i); + } + done(); + }); + }); + }); + }); + + it("should arrive when binary data is sent as Int32Array (ws)", (done) => { + const binaryData = new Int32Array(5); + for (let i = 0; i < binaryData.length; i++) { + binaryData[i] = (i + 100) * 9823; + } + + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.on("connection", (conn) => { + conn.send(binaryData); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + let i = 0, + ii = 0; + for (; ii < binaryData.length; i += 4, ii++) { + const num = msg.readInt32LE(i); + expect(num).to.be((ii + 100) * 9823); + } + done(); + }); + }); + }); + }); + + it("should arrive when binary data is sent as Int32Array, given as ArrayBuffer(ws)", (done) => { + const binaryData = new Int32Array(5); + for (let i = 0; i < binaryData.length; i++) { + binaryData[i] = (i + 100) * 9823; + } + + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.on("connection", (conn) => { + conn.send(binaryData.buffer); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + let i = 0, + ii = 0; + for (; ii < binaryData.length; i += 4, ii++) { + const num = msg.readInt32LE(i); + expect(num).to.be((ii + 100) * 9823); + } + done(); + }); + }); + }); + }); + + it("should arrive when binary data is sent as Buffer (ws)", (done) => { + const binaryData = Buffer.allocUnsafe(5); + for (let i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.on("connection", (conn) => { + conn.send(binaryData); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + for (let i = 0; i < binaryData.length; i++) { + const num = msg.readInt8(i); + expect(num).to.be(i); + } + done(); + }); + }); + }); + }); + + it("should arrive when binary data sent as Buffer (polling)", (done) => { + const binaryData = Buffer.allocUnsafe(5); + for (let i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + const opts = { allowUpgrades: false, transports: ["polling"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + + engine.on("connection", (conn) => { + conn.send(binaryData); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + for (let i = 0; i < binaryData.length; i++) { + const num = msg.readInt8(i); + expect(num).to.be(i); + } + + done(); + }); + }); + }); + }); + + it("should arrive as ArrayBuffer if requested when binary data sent as Buffer (ws)", (done) => { + const binaryData = Buffer.allocUnsafe(5); + for (let i = 0; i < binaryData.length; i++) { + binaryData.writeInt8(i, i); + } + + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.binaryType = "arraybuffer"; + + engine.on("connection", (conn) => { + conn.send(binaryData); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg instanceof ArrayBuffer).to.be(true); + const intArray = new Int8Array(msg); + for (let i = 0; i < binaryData.length; i++) { + expect(intArray[i]).to.be(i); + } + + done(); + }); + }); + }); + }); + + 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++) { + binaryData.writeInt8(i, i); + } + + const opts = { allowUpgrades: false, transports: ["polling"] }; + const engine = listen(opts, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + socket.binaryType = "arraybuffer"; + + engine.on("connection", (conn) => { + conn.send(binaryData); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg instanceof ArrayBuffer).to.be(true); + const intArray = new Int8Array(msg); + for (let i = 0; i < binaryData.length; i++) { + expect(intArray[i]).to.be(i); + } + + done(); + }); + }); + }); + }); + + it("should trigger a flush/drain event", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + engine.on("connection", (socket) => { + let totalEvents = 4; + + engine.on("flush", (sock, buf) => { + expect(sock).to.be(socket); + expect(buf).to.be.an("array"); + --totalEvents || done(); + }); + socket.on("flush", (buf) => { + expect(buf).to.be.an("array"); + --totalEvents || done(); + }); + + engine.on("drain", (sock) => { + expect(sock).to.be(socket); + expect(socket.writeBuffer.length).to.be(0); + --totalEvents || done(); + }); + socket.on("drain", () => { + expect(socket.writeBuffer.length).to.be(0); + --totalEvents || done(); + }); + + socket.send("aaaa"); + }); + + new ClientSocket(`ws://localhost:${port}`); + }); + }); + + it( + "should interleave with pongs if many messages buffered " + + "after connection open", + function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + this.slow(4000); + this.timeout(8000); + + const opts = { + transports: ["websocket"], + pingInterval: 200, + pingTimeout: 100, + }; + + const engine = listen(opts, (port) => { + const messageCount = 100; + const messagePayload = new Array(256 * 256).join("a"); + let connection = null; + engine.on("connection", (conn) => { + connection = conn; + }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + socket.on("open", () => { + for (let i = 0; i < messageCount; i++) { + // connection.send('message: ' + i); // works + connection.send(messagePayload + "|message: " + i); // does not work + } + let receivedCount = 0; + socket.on("message", (msg) => { + receivedCount += 1; + if (receivedCount === messageCount) { + done(); + } + }); + }); + }); + } + ); + + it("should support chinese", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + const shi = "石室詩士施氏,嗜獅,誓食十獅。"; + const shi2 = "氏時時適市視獅。"; + engine.on("connection", (conn) => { + conn.send("."); + conn.send(shi); + conn.send(shi2); + conn.once("message", (msg0) => { + expect(msg0).to.be("."); + conn.once("message", (msg) => { + expect(msg).to.be(shi); + conn.once("message", (msg2) => { + expect(msg2).to.be(shi2); + done(); + }); + }); + }); + }); + socket.on("open", () => { + socket.once("message", (msg0) => { + expect(msg0).to.be("."); + socket.once("message", (msg) => { + expect(msg).to.be(shi); + socket.once("message", (msg2) => { + expect(msg2).to.be(shi2); + socket.send("."); + socket.send(shi); + socket.send(shi2); + }); + }); + }); + }); + }); + }); + + it("should send and receive data with key and cert (polling)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const srvOpts = { + key: fs.readFileSync("test/fixtures/server.key"), + cert: fs.readFileSync("test/fixtures/server.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + requestCert: true, + rejectUnauthorized: true, + }; + + const opts = { + key: fs.readFileSync("test/fixtures/client.key"), + cert: fs.readFileSync("test/fixtures/client.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["polling"], + }; + + const srv = https.createServer(srvOpts, (req, res) => { + res.writeHead(200); + res.end("hello world\n"); + }); + + const engine = new Server({ + transports: ["polling"], + allowUpgrades: false, + allowEIO3: true, + }); + engine.attach(srv); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + expect(msg).to.be("hello"); + done(); + }); + }); + + socket.on("open", () => { + socket.send("hello"); + }); + }); + }); + + it("should send and receive data with ca when not requiring auth (polling)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const srvOpts = { + key: fs.readFileSync("test/fixtures/server.key"), + cert: fs.readFileSync("test/fixtures/server.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + requestCert: true, + rejectUnauthorized: false, + }; + + const opts = { + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["polling"], + }; + + const srv = https.createServer(srvOpts, (req, res) => { + res.writeHead(200); + res.end("hello world\n"); + }); + + const engine = new Server({ + transports: ["polling"], + allowUpgrades: false, + allowEIO3: true, + }); + engine.attach(srv); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + expect(msg).to.be("hello"); + done(); + }); + }); + + socket.on("open", () => { + socket.send("hello"); + }); + }); + }); + + it("should send and receive data with key and cert (ws)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const srvOpts = { + key: fs.readFileSync("test/fixtures/server.key"), + cert: fs.readFileSync("test/fixtures/server.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + requestCert: true, + rejectUnauthorized: true, + }; + + const opts = { + key: fs.readFileSync("test/fixtures/client.key"), + cert: fs.readFileSync("test/fixtures/client.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["websocket"], + }; + + const srv = https.createServer(srvOpts, (req, res) => { + res.writeHead(200); + res.end("hello world\n"); + }); + + const engine = new Server({ + transports: ["websocket"], + allowUpgrades: false, + allowEIO3: true, + }); + engine.attach(srv); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + expect(msg).to.be("hello"); + done(); + }); + }); + + socket.on("open", () => { + socket.send("hello"); + }); + }); + }); + + it("should send and receive data with pfx (polling)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const srvOpts = { + key: fs.readFileSync("test/fixtures/server.key"), + cert: fs.readFileSync("test/fixtures/server.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + requestCert: true, + rejectUnauthorized: true, + }; + + const opts = { + pfx: fs.readFileSync("test/fixtures/client.pfx"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["polling"], + }; + + const srv = https.createServer(srvOpts, (req, res) => { + res.writeHead(200); + res.end("hello world\n"); + }); + + const engine = new Server({ + transports: ["polling"], + allowUpgrades: false, + allowEIO3: true, + }); + engine.attach(srv); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + expect(msg).to.be("hello"); + done(); + }); + }); + + socket.on("open", () => { + socket.send("hello"); + }); + }); + }); + + it("should send and receive data with pfx (ws)", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const srvOpts = { + key: fs.readFileSync("test/fixtures/server.key"), + cert: fs.readFileSync("test/fixtures/server.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + requestCert: true, + rejectUnauthorized: true, + }; + + const opts = { + pfx: fs.readFileSync("test/fixtures/client.pfx"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["websocket"], + }; + + const srv = https.createServer(srvOpts, (req, res) => { + res.writeHead(200); + res.end("hello world\n"); + }); + + const engine = new Server({ + transports: ["websocket"], + allowUpgrades: false, + allowEIO3: true, + }); + engine.attach(srv); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + expect(msg).to.be("hello"); + done(); + }); + }); + + socket.on("open", () => { + socket.send("hello"); + }); + }); + }); + }); + + describe("send", () => { + describe("writeBuffer", () => { + it("should not empty until `drain` event (polling)", (done) => { + listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + let totalEvents = 2; + socket.on("open", () => { + socket.send("a"); + socket.send("b"); + // writeBuffer should be nonempty, with 'a' still in it + expect(socket.writeBuffer.length).to.eql(2); + }); + socket.transport.on("drain", () => { + expect(socket.writeBuffer.length).to.eql(--totalEvents); + totalEvents || done(); + }); + }); + }); + + it("should not empty until `drain` event (websocket)", (done) => { + listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let totalEvents = 2; + socket.on("open", () => { + socket.send("a"); + socket.send("b"); + // writeBuffer should be nonempty, with 'a' still in it + expect(socket.writeBuffer.length).to.eql(2); + }); + socket.transport.on("drain", () => { + expect(socket.writeBuffer.length).to.eql(--totalEvents); + totalEvents || done(); + }); + }); + }); + }); + + describe("callback", () => { + it("should execute in order when message sent (client) (polling)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + let i = 0; + let j = 0; + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + conn.send(msg); + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + // send another packet until we've sent 3 total + if (++i < 3) { + expect(i).to.eql(j); + sendFn(); + } else { + done(); + } + }); + + function sendFn() { + socket.send( + j, + ((value) => { + j++; + })(j) + ); + } + + sendFn(); + }); + }); + }); + + it("should execute in order when message sent (client) (websocket)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let i = 0; + let j = 0; + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + conn.send(msg); + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + // send another packet until we've sent 3 total + if (++i < 3) { + expect(i).to.eql(j); + sendFn(); + } else { + done(); + } + }); + + function sendFn() { + socket.send( + j, + ((value) => { + j++; + })(j) + ); + } + + sendFn(); + }); + }); + }); + + it("should execute in order with payloads (client) (polling)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + let i = 0; + let lastCbFired = 0; + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + conn.send(msg); + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.eql(i + 1); + i++; + }); + + function cb(value) { + expect(value).to.eql(lastCbFired + 1); + lastCbFired = value; + if (value === 3) { + done(); + } + } + + // 2 and 3 will be in the same payload + socket.once("flush", () => { + socket.send(2, () => { + cb(2); + }); + socket.send(3, () => { + cb(3); + }); + }); + + socket.send(1, () => { + cb(1); + }); + }); + }); + }); + + it("should execute in order with payloads (client) (websocket)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let i = 0; + let lastCbFired = 0; + + engine.on("connection", (conn) => { + conn.on("message", (msg) => { + conn.send(msg); + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.eql(i + 1); + i++; + }); + + function cb(value) { + expect(value).to.eql(lastCbFired + 1); + lastCbFired = value; + if (value === 3) { + done(); + } + } + + // 2 and 3 will be in the same payload + socket.once("flush", () => { + socket.send(2, () => { + cb(2); + }); + socket.send(3, () => { + cb(3); + }); + }); + + socket.send(1, () => { + cb(1); + }); + }); + }); + }); + + it("should execute when message sent (polling)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + let i = 0; + let j = 0; + + engine.on("connection", (conn) => { + conn.send("a", (transport) => { + i++; + }); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + j++; + }); + }); + + setTimeout(() => { + expect(i).to.be(j); + done(); + }, 100); + }); + }); + + it("should execute when message sent during polling upgrade window", (done) => { + const engine = listen((port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling", "websocket"], + }); + + const partialDone = createPartialDone(() => { + engine.httpServer?.close(); + socket.close(); + done(); + }, 2); + + engine.on("connection", (conn) => { + conn.on("upgrading", () => { + conn.send("a", partialDone); + }); + }); + socket.on("open", () => { + socket.on("message", partialDone); + }); + }); + }); + + it("should execute when message sent (websocket)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let i = 0; + let j = 0; + + engine.on("connection", (conn) => { + conn.send("a", (transport) => { + i++; + }); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + j++; + }); + }); + + setTimeout(() => { + expect(i).to.be(j); + done(); + }, 100); + }); + }); + + it("should execute once for each send", (done) => { + const engine = listen((port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let a = 0; + let b = 0; + let c = 0; + let all = 0; + + engine.on("connection", (conn) => { + conn.send("a"); + conn.send("b"); + conn.send("c"); + }); + + socket.on("open", () => { + socket.on("message", (msg) => { + if (msg === "a") a++; + if (msg === "b") b++; + if (msg === "c") c++; + + if (++all === 3) { + expect(a).to.be(1); + expect(b).to.be(1); + expect(c).to.be(1); + done(); + } + }); + }); + }); + }); + + it("should execute in multipart packet (websocket)", (done) => { + const engine = listen((port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + let i = 0; + let j = 0; + + engine.on("connection", (conn) => { + conn.send("d", (transport) => { + i++; + }); + + conn.send("c", (transport) => { + i++; + }); + + conn.send("b", (transport) => { + i++; + }); + + conn.send("a", (transport) => { + i++; + }); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + j++; + }); + }); + + setTimeout(() => { + expect(i).to.be(j); + done(); + }, 200); + }); + }); + + it("should execute in multipart packet (polling)", (done) => { + const engine = listen((port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + let i = 0; + let j = 0; + + engine.on("connection", (conn) => { + conn.send("d", (transport) => { + i++; + }); + + conn.send("c", (transport) => { + i++; + }); + + conn.send("b", (transport) => { + i++; + }); + + conn.send("a", (transport) => { + i++; + }); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + j++; + }); + }); + + setTimeout(() => { + expect(i).to.be(j); + done(); + }, 200); + }); + }); + + it("should clean callback references when socket gets closed with pending callbacks", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + + engine.on("connection", (conn) => { + socket.transport.on("pollComplete", () => { + conn.send("a", (transport) => { + done(new Error("Test invalidation")); + }); + + if (!conn.writeBuffer.length) { + done(new Error("Test invalidation")); + } + + // force to close the socket when we have one or more packet(s) in buffer + socket.close(); + }); + + conn.on("close", (reason) => { + expect(conn.packetsFn).to.be.empty(); + expect(conn.sentCallbackFn).to.be.empty(); + done(); + }); + }); + }); + }); + + it("should not execute when it is not actually sent (polling)", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + + socket.transport.on("pollComplete", (msg) => { + socket.close(); + }); + + engine.on("connection", (conn) => { + let err; + conn.send("a"); + conn.send("b", (transport) => { + err = new Error("Test invalidation"); + }); + conn.on("close", (reason) => { + done(err); + }); + }); + }); + }); + }); + + describe("pre-encoded content", () => { + it("should use the pre-encoded frame", function (done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + engine = listen((port) => { + client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.on("connection", (conn) => { + conn.send("test", { + wsPreEncodedFrame: [ + Buffer.from([129, 4]), + Buffer.from([52, 49, 50, 51]), + ], + }); + }); + + client.on("message", (msg) => { + expect(msg).to.be("123"); + done(); + }); + }); + }); + + it("should not use the pre-encoded frame when the permessage-deflate extension is enabled", (done) => { + engine = listen({ perMessageDeflate: true }, (port) => { + client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + engine.on("connection", (conn) => { + conn.send("test", { + wsPreEncodedFrame: [ + Buffer.from([129, 4]), + Buffer.from([52, 49, 50, 51]), + ], + }); + }); + + client.on("message", (msg) => { + expect(msg).to.be("test"); + done(); + }); + }); + }); + }); + }); + + describe("packet", () => { + it("should emit when socket receives packet", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + const socket = new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("packet", (packet) => { + expect(packet.type).to.be("message"); + expect(packet.data).to.be("a"); + done(); + }); + }); + socket.on("open", () => { + socket.send("a"); + }); + }); + }); + + it("should emit when receives pong", (done) => { + const engine = listen( + { allowUpgrades: false, pingInterval: 4 }, + (port) => { + new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("packet", (packet) => { + conn.close(); + if (process.env.EIO_CLIENT === "3") { + expect(packet.type).to.be("ping"); + } else { + expect(packet.type).to.be("pong"); + } + done(); + }); + }); + } + ); + }); + }); + + describe("packetCreate", () => { + it("should emit before socket send message", (done) => { + const engine = listen({ allowUpgrades: false }, (port) => { + new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("packetCreate", (packet) => { + expect(packet.type).to.be("message"); + expect(packet.data).to.be("a"); + done(); + }); + conn.send("a"); + }); + }); + }); + + it("should emit before send pong", (done) => { + const engine = listen( + { allowUpgrades: false, pingInterval: 4 }, + (port) => { + new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.on("packetCreate", (packet) => { + conn.close(); + if (process.env.EIO_CLIENT === "3") { + expect(packet.type).to.be("pong"); + } else { + expect(packet.type).to.be("ping"); + } + done(); + }); + }); + } + ); + }); + }); + + describe("upgrade", () => { + it("should upgrade", (done) => { + const engine = listen((port) => { + // it takes both to send 50 to verify + let ready = 2; + let closed = 2; + + function finish() { + setTimeout(() => { + socket.close(); + }, 10); + } + + // server + engine.on("connection", (conn) => { + let lastSent = 0; + let lastReceived = 0; + let upgraded = false; + const interval = setInterval(() => { + lastSent++; + conn.send(lastSent); + if (50 === lastSent) { + clearInterval(interval); + --ready || finish(); + } + }, 2); + + expect(conn.request._query.transport).to.be("polling"); + + conn.on("message", (msg) => { + expect(conn.request._query).to.be.an("object"); + lastReceived++; + expect(msg).to.eql(lastReceived); + }); + + conn.on("upgrade", (to) => { + expect(conn.request._query.transport).to.be("polling"); + upgraded = true; + expect(to.name).to.be("websocket"); + expect(conn.transport.name).to.be("websocket"); + }); + + conn.on("close", (reason) => { + expect(reason).to.be("transport close"); + expect(lastSent).to.be(50); + expect(lastReceived).to.be(50); + expect(upgraded).to.be(true); + --closed || done(); + }); + }); + + // client + var socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + let lastSent = 0; + let lastReceived = 0; + let upgrades = 0; + const interval = setInterval(() => { + lastSent++; + socket.send(lastSent); + if (50 === lastSent) { + clearInterval(interval); + --ready || finish(); + } + }, 2); + socket.on("upgrading", (to) => { + // we want to make sure for the sake of this test that we have a buffer + expect(to.name).to.equal("websocket"); + upgrades++; + + // force send a few packets to ensure we test buffer transfer + lastSent++; + socket.send(lastSent); + lastSent++; + socket.send(lastSent); + + expect(socket.writeBuffer).to.not.be.empty(); + }); + socket.on("upgrade", (to) => { + expect(to.name).to.equal("websocket"); + upgrades++; + }); + socket.on("message", (msg) => { + lastReceived++; + expect(lastReceived).to.eql(msg); + }); + socket.on("close", (reason) => { + expect(reason).to.be("forced close"); + expect(lastSent).to.be(50); + expect(upgrades).to.be(2); + --closed || done(); + }); + }); + }); + + if (engine.httpServer) { + // attach another engine to make sure it doesn't break upgrades + attach(engine.httpServer, { path: "/foo" }); + } + }); + }); + + describe("http compression", () => { + function getSidFromResponse(res) { + const c = cookieMod.parse(res.headers["set-cookie"][0]); + return c[Object.keys(c)[0]]; + } + + it("should compress by default", (done) => { + const engine = listen( + { cookie: true, transports: ["polling"] }, + (port) => { + engine.on("connection", (conn) => { + const buf = Buffer.allocUnsafe(1024); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf); + }); + + http.get( + { + port: port, + path: "/engine.io/?transport=polling", + }, + (res) => { + const sid = getSidFromResponse(res); + http.get( + { + port: port, + path: "/engine.io/?transport=polling&sid=" + sid, + headers: { "Accept-Encoding": "gzip, deflate" }, + }, + (res) => { + expect(res.headers["content-encoding"]).to.equal("gzip"); + res + .pipe(zlib.createGunzip()) + .on("error", done) + .on("end", done) + .resume(); + } + ); + } + ); + } + ); + }); + + it("should compress using deflate", (done) => { + const engine = listen( + { cookie: true, transports: ["polling"] }, + (port) => { + engine.on("connection", (conn) => { + const buf = Buffer.allocUnsafe(1024); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf); + }); + + http.get( + { + port: port, + path: "/engine.io/?transport=polling", + }, + (res) => { + const sid = getSidFromResponse(res); + http.get( + { + port: port, + path: "/engine.io/?transport=polling&sid=" + sid, + headers: { "Accept-Encoding": "deflate" }, + }, + (res) => { + expect(res.headers["content-encoding"]).to.equal("deflate"); + res + .pipe(zlib.createDeflate()) + .on("error", done) + .on("end", done) + .resume(); + } + ); + } + ); + } + ); + }); + + it("should set threshold", (done) => { + const engine = listen( + { + cookie: true, + transports: ["polling"], + httpCompression: { threshold: 0 }, + }, + (port) => { + engine.on("connection", (conn) => { + const buf = Buffer.allocUnsafe(10); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf); + }); + + http.get( + { + port: port, + path: "/engine.io/?transport=polling", + }, + (res) => { + const sid = getSidFromResponse(res); + http.get( + { + port: port, + path: "/engine.io/?transport=polling&sid=" + sid, + headers: { "Accept-Encoding": "gzip, deflate" }, + }, + (res) => { + expect(res.headers["content-encoding"]).to.equal("gzip"); + done(); + } + ); + } + ); + } + ); + }); + + it("should disable compression", (done) => { + const engine = listen( + { cookie: true, transports: ["polling"], httpCompression: false }, + (port) => { + engine.on("connection", (conn) => { + const buf = Buffer.allocUnsafe(1024); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf); + }); + + http.get( + { + port: port, + path: "/engine.io/?transport=polling", + }, + (res) => { + const sid = getSidFromResponse(res); + http.get( + { + port: port, + path: "/engine.io/?transport=polling&sid=" + sid, + headers: { "Accept-Encoding": "gzip, deflate" }, + }, + (res) => { + expect(res.headers["content-encoding"]).to.be(undefined); + done(); + } + ); + } + ); + } + ); + }); + + it("should disable compression per message", (done) => { + const engine = listen( + { cookie: true, transports: ["polling"] }, + (port) => { + engine.on("connection", (conn) => { + const buf = Buffer.allocUnsafe(1024); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf, { compress: false }); + }); + + http.get( + { + port: port, + path: "/engine.io/?transport=polling", + }, + (res) => { + const sid = getSidFromResponse(res); + http.get( + { + port: port, + path: "/engine.io/?transport=polling&sid=" + sid, + headers: { "Accept-Encoding": "gzip, deflate" }, + }, + (res) => { + expect(res.headers["content-encoding"]).to.be(undefined); + done(); + } + ); + } + ); + } + ); + }); + + it("should not compress when the byte size is below threshold", (done) => { + const engine = listen( + { cookie: true, transports: ["polling"] }, + (port) => { + engine.on("connection", (conn) => { + const buf = Buffer.allocUnsafe(100); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf); + }); + + http.get( + { + port: port, + path: "/engine.io/?transport=polling", + }, + (res) => { + const sid = getSidFromResponse(res); + http.get( + { + port: port, + path: "/engine.io/?transport=polling&sid=" + sid, + headers: { "Accept-Encoding": "gzip, deflate" }, + }, + (res) => { + expect(res.headers["content-encoding"]).to.be(undefined); + done(); + } + ); + } + ); + } + ); + }); + }); + + describe("extraHeaders", function () { + this.timeout(5000); + + const headers = { + "x-custom-header-for-my-project": "my-secret-access-token", + cookie: + "user_session=NI2JlCKF90aE0sJZD9ZzujtdsUqNYSBYxzlTsvdSUe35ZzdtVRGqYFr0kdGxbfc5gUOkR9RGp20GVKza; path=/; expires=Tue, 07-Apr-2015 18:18:08 GMT; secure; HttpOnly", + }; + + function testForTransport(transport, done) { + const engine = listen((port) => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + extraHeaders: headers, + transports: [transport], + }); + engine.on("connection", (conn) => { + for (let h in headers) { + expect(conn.request.headers[h]).to.equal(headers[h]); + } + done(); + }); + socket.on("open", () => {}); + }); + } + + it("should arrive from client to server via WebSockets", (done) => { + testForTransport("websocket", done); + }); + + it("should arrive from client to server via XMLHttpRequest", (done) => { + testForTransport("polling", done); + }); + }); + + describe("response headers", () => { + function testForHeaders(headers, callback) { + const engine = listen((port) => { + engine.on("connection", (conn) => { + conn.transport.once("headers", (headers) => { + callback(headers); + conn.close(); + }); + conn.send("hi"); + }); + new ClientSocket(`ws://localhost:${port}`, { + extraHeaders: headers, + transports: ["polling"], + }); + }); + } + + it("should contain X-XSS-Protection: 0 for IE8", (done) => { + const headers = { + "user-agent": + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0)", + }; + testForHeaders(headers, (headers) => { + expect(headers["X-XSS-Protection"]).to.be("0"); + done(); + }); + }); + + it("should contain X-XSS-Protection: 0 for IE11", (done) => { + const headers = { + "user-agent": + "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", + }; + testForHeaders(headers, (headers) => { + expect(headers["X-XSS-Protection"]).to.be("0"); + done(); + }); + }); + + it("should include a 'cache-control' header", (done) => { + testForHeaders({}, (headers) => { + expect(headers["cache-control"]).to.be("no-store"); + done(); + }); + }); + + it("should emit a 'initial_headers' event (polling)", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen({ cookie: true }, (port) => { + engine.on("initial_headers", (headers, req) => { + expect(req.method).to.be("GET"); + headers["test"] = "123"; + headers["set-cookie"] = "mycookie=456"; + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(res.headers["test"]).to.be("123"); + expect(res.headers["set-cookie"].length).to.be(2); + expect(res.headers["set-cookie"][1]).to.be("mycookie=456"); + + const sid = JSON.parse(res.text.slice(5)).sid; + + request + .post(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", sid }) + .send("1:6") + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(res.headers["test"]).to.be(undefined); + expect(res.headers["set-cookie"]).to.be(undefined); + partialDone(); + }); + }); + }); + }); + + it("should emit a 'headers' event (polling)", (done) => { + const partialDone = createPartialDone(done, 3); + + engine = listen({ cookie: true }, (port) => { + engine.on("headers", (headers) => { + headers["test"] = "123"; + headers["set-cookie"] = "mycookie=456"; + partialDone(); + }); + + request + .get(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(res.headers["test"]).to.be("123"); + expect(res.headers["set-cookie"].length).to.be(2); + expect(res.headers["set-cookie"][1]).to.be("mycookie=456"); + + const sid = JSON.parse(res.text.slice(5)).sid; + + request + .post(`http://localhost:${port}/engine.io/`) + .query({ transport: "polling", sid }) + .send("1:6") + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(res.headers["set-cookie"].length).to.be(1); + expect(res.headers["set-cookie"][0]).to.be("mycookie=456"); + partialDone(); + }); + }); + }); + }); + + it("should emit a 'initial_headers' event (websocket)", function (done) { + if ( + process.env.EIO_WS_ENGINE === "eiows" || + process.env.EIO_WS_ENGINE === "uws" + ) { + return this.skip(); + } + const partialDone = createPartialDone(done, 2); + + engine = listen({ cookie: true }, (port) => { + engine.on("initial_headers", (headers, req) => { + expect(req.method).to.be("GET"); + headers["test"] = "123"; + headers["set-cookie"] = "mycookie=456"; + partialDone(); + }); + + client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + + client.transport.ws.on("upgrade", (res) => { + expect(res.headers["test"]).to.be("123"); + expect(res.headers["set-cookie"].length).to.be(1); + expect(res.headers["set-cookie"][0]).to.be("mycookie=456"); + partialDone(); + }); + }); + }); + + it("should emit a single 'initial_headers' event per connection", (done) => { + const partialDone = createPartialDone(done, 2); + + engine = listen((port) => { + engine.on("initial_headers", () => { + partialDone(); + }); + + client = new ClientSocket(`ws://localhost:${port}`); + + client.on("upgrade", () => { + partialDone(); + }); + }); + }); + + it("should emit several 'headers' events per connection", function (done) { + if ( + process.env.EIO_WS_ENGINE === "eiows" || + process.env.EIO_WS_ENGINE === "uws" + ) { + return this.skip(); + } + const partialDone = createPartialDone(done, 4); + + engine = listen((port) => { + engine.on("headers", () => { + partialDone(); + }); + + client = new ClientSocket(`ws://localhost:${port}`); + + client.on("upgrade", () => { + partialDone(); + }); + }); + }); + }); + + describe("cors", () => { + it("should allow CORS from the current origin (preflight request)", (done) => { + listen( + { cors: { origin: true, headers: ["my-header"], credentials: true } }, + (port) => { + request + .options(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io") + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(204); + expect(res.body).to.be.empty(); + expect(res.header["access-control-allow-origin"]).to.be( + "http://engine.io" + ); + expect(res.header["access-control-allow-methods"]).to.be( + "GET,HEAD,PUT,PATCH,POST,DELETE" + ); + expect(res.header["access-control-allow-headers"]).to.be( + "my-header" + ); + expect(res.header["access-control-allow-credentials"]).to.be( + "true" + ); + done(); + }); + } + ); + }); + + it("should allow CORS from the current origin (actual request)", (done) => { + listen( + { cors: { origin: true, headers: ["my-header"], credentials: true } }, + (port) => { + request + .get(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://engine.io") + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(res.body).to.be.empty(); + expect(res.header["access-control-allow-origin"]).to.be( + "http://engine.io" + ); + expect(res.header["access-control-allow-methods"]).to.be( + undefined + ); + expect(res.header["access-control-allow-headers"]).to.be( + undefined + ); + expect(res.header["access-control-allow-credentials"]).to.be( + "true" + ); + done(); + }); + } + ); + }); + + it("should disallow CORS from a bad origin", (done) => { + listen( + { + cors: { + origin: ["http://good-domain.com"], + }, + }, + (port) => { + request + .options(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://bad-domain.com") + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(204); + expect(res.body).to.be.empty(); + expect(res.header["access-control-allow-origin"]).to.be( + undefined + ); + expect(res.header["access-control-allow-credentials"]).to.be( + undefined + ); + done(); + }); + } + ); + }); + + it("should forward the configuration to the cors module", (done) => { + listen( + { + cors: { + origin: "http://good-domain.com", + methods: ["GET", "PUT", "POST"], + allowedHeaders: ["my-header"], + exposedHeaders: ["my-exposed-header"], + credentials: true, + maxAge: 123, + optionsSuccessStatus: 200, + }, + }, + (port) => { + request + .options(`http://localhost:${port}/engine.io/`) + .set("Origin", "http://good-domain.com") + .query({ transport: "polling" }) + .end((err, res) => { + expect(err).to.be(null); + expect(res.status).to.be(200); + expect(res.body).to.be.empty(); + expect(res.header["access-control-allow-origin"]).to.be( + "http://good-domain.com" + ); + expect(res.header["access-control-allow-methods"]).to.be( + "GET,PUT,POST" + ); + expect(res.header["access-control-allow-headers"]).to.be( + "my-header" + ); + expect(res.header["access-control-expose-headers"]).to.be( + "my-exposed-header" + ); + expect(res.header["access-control-allow-credentials"]).to.be( + "true" + ); + expect(res.header["access-control-max-age"]).to.be("123"); + done(); + }); + } + ); + }); + + it("should work with CORS enabled", (done) => { + engine = listen( + { cors: { origin: true, headers: ["my-header"], credentials: true } }, + (port) => { + const client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + engine.on("connection", (socket) => { + socket.on("message", (msg) => { + expect(msg).to.be("hey"); + socket.send("holà"); + }); + }); + client.on("open", () => { + client.send("hey"); + }); + client.on("message", (msg) => { + expect(msg).to.be("holà"); + client.close(); + done(); + }); + } + ); + }); + }); + + describe("wsEngine option", () => { + before(function () { + if (process.env.EIO_WS_ENGINE === "uws") { + this.skip(); + } + }); + + // FIXME eiows fails to build on Node.js 18 (and has dropped support for Node.js 10) + it.skip("should allow loading of other websocket server implementation like eiows", (done) => { + const engine = listen( + { allowUpgrades: false, wsEngine: require("eiows").Server }, + (port) => { + expect(engine.ws instanceof require("eiows").Server).to.be.ok(); + const socket = new ClientSocket(`ws://localhost:${port}`); + engine.on("connection", (conn) => { + conn.send("a"); + }); + socket.on("open", () => { + socket.on("message", (msg) => { + expect(msg).to.be("a"); + done(); + }); + }); + } + ); + }); + }); + + describe("remoteAddress", () => { + const POSSIBLE_VALUES = [ + "0000:0000:0000:0000:0000:0000:0000:0001", + "0000:0000:0000:0000:0000:ffff:7f00:0001", + "::ffff:127.0.0.1", + "::1", + ]; + + it("should be defined (polling)", (done) => { + const engine = listen({ transports: ["polling"] }, (port) => { + new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"], + }); + engine.on("connection", (socket) => { + expect(POSSIBLE_VALUES).to.contain(socket.remoteAddress); + done(); + }); + }); + }); + + it("should be defined (ws)", (done) => { + const engine = listen({ transports: ["websocket"] }, (port) => { + new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"], + }); + engine.on("connection", (socket) => { + expect(POSSIBLE_VALUES).to.contain(socket.remoteAddress); + done(); + }); + }); + }); + }); +}); diff --git a/packages/engine.io/test/util.mjs b/packages/engine.io/test/util.mjs new file mode 100644 index 00000000..01f6e3ea --- /dev/null +++ b/packages/engine.io/test/util.mjs @@ -0,0 +1,404 @@ +// imported from https://github.com/fails-components/webtransport/blob/master/test/fixtures/certificate.js + +// @ts-expect-error node-forge has no types and @types/node-forge do not include oids +import forge from 'node-forge' +import { webcrypto as crypto, X509Certificate } from 'crypto' + +const { pki, asn1, oids } = forge +// taken from node-forge +/** + * Converts an X.509 subject or issuer to an ASN.1 RDNSequence. + * + * @param {any} obj the subject or issuer (distinguished name). + * + * @return the ASN.1 RDNSequence. + */ +function _dnToAsn1(obj) { + // create an empty RDNSequence + const rval = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, []) + + // iterate over attributes + let attr, set + const attrs = obj.attributes + for (let i = 0; i < attrs.length; ++i) { + attr = attrs[i] + let value = attr.value + + // reuse tag class for attribute value if available + let valueTagClass = asn1.Type.PRINTABLESTRING + if ('valueTagClass' in attr) { + valueTagClass = attr.valueTagClass + + if (valueTagClass === asn1.Type.UTF8) { + value = forge.util.encodeUtf8(value) + } + // FIXME: handle more encodings + } + + // create a RelativeDistinguishedName set + // each value in the set is an AttributeTypeAndValue first + // containing the type (an OID) and second the value + set = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // AttributeType + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.OID, + false, + asn1.oidToDer(attr.type).getBytes() + ), + // AttributeValue + asn1.create(asn1.Class.UNIVERSAL, valueTagClass, false, value) + ]) + ]) + rval.value.push(set) + } + + return rval +} + +const jan_1_1950 = new Date('1950-01-01T00:00:00Z') // eslint-disable-line camelcase +const jan_1_2050 = new Date('2050-01-01T00:00:00Z') // eslint-disable-line camelcase +// taken from node-forge almost not modified +/** + * Converts a Date object to ASN.1 + * Handles the different format before and after 1st January 2050 + * + * @param {Date} date date object. + * + * @return the ASN.1 object representing the date. + */ +function _dateToAsn1(date) { + // eslint-disable-next-line camelcase + if (date >= jan_1_1950 && date < jan_1_2050) { + return asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.UTCTIME, + false, + asn1.dateToUtcTime(date) + ) + } else { + return asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.GENERALIZEDTIME, + false, + asn1.dateToGeneralizedTime(date) + ) + } +} + +// taken from node-forge almost not modified +/** + * Convert signature parameters object to ASN.1 + * + * @param {string} oid Signature algorithm OID + * @param {any} params The signature parameters object + * @return ASN.1 object representing signature parameters + */ +function _signatureParametersToAsn1(oid, params) { + const parts = [] + + switch (oid) { + case oids['RSASSA-PSS']: + if (params.hash.algorithmOid !== undefined) { + parts.push( + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.OID, + false, + asn1.oidToDer(params.hash.algorithmOid).getBytes() + ), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + ]) + ]) + ) + } + + if (params.mgf.algorithmOid !== undefined) { + parts.push( + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.OID, + false, + asn1.oidToDer(params.mgf.algorithmOid).getBytes() + ), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.OID, + false, + asn1.oidToDer(params.mgf.hash.algorithmOid).getBytes() + ), + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + ]) + ]) + ]) + ) + } + + if (params.saltLength !== undefined) { + parts.push( + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.INTEGER, + false, + asn1.integerToDer(params.saltLength).getBytes() + ) + ]) + ) + } + + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, parts) + + default: + return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '') + } +} + +// taken from node-forge and modified to work with ECDSA +/** + * Gets the ASN.1 TBSCertificate part of an X.509v3 certificate. + * + * @param {any} cert the certificate. + * + * @return the asn1 TBSCertificate. + */ +function getTBSCertificate(cert) { + // TBSCertificate + const notBefore = _dateToAsn1(cert.validity.notBefore) + const notAfter = _dateToAsn1(cert.validity.notAfter) + + const tbs = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // version + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [ + // integer + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.INTEGER, + false, + asn1.integerToDer(cert.version).getBytes() + ) + ]), + // serialNumber + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.INTEGER, + false, + forge.util.hexToBytes(cert.serialNumber) + ), + // signature + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + // algorithm + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.OID, + false, + asn1.oidToDer(cert.siginfo.algorithmOid).getBytes() + ), + // parameters + _signatureParametersToAsn1( + cert.siginfo.algorithmOid, + cert.siginfo.parameters + ) + ]), + // issuer + _dnToAsn1(cert.issuer), + // validity + asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [ + notBefore, + notAfter + ]), + // subject + _dnToAsn1(cert.subject), + // SubjectPublicKeyInfo + // here comes our modification, we are other objects here + asn1.fromDer( + new forge.util.ByteBuffer( + cert.publicKey + ) /* is in already SPKI format but in DER encoding */ + ) + ]) + + if (cert.issuer.uniqueId) { + // issuerUniqueID (optional) + tbs.value.push( + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [ + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.BITSTRING, + false, + // TODO: support arbitrary bit length ids + String.fromCharCode(0x00) + cert.issuer.uniqueId + ) + ]) + ) + } + if (cert.subject.uniqueId) { + // subjectUniqueID (optional) + tbs.value.push( + asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [ + asn1.create( + asn1.Class.UNIVERSAL, + asn1.Type.BITSTRING, + false, + // TODO: support arbitrary bit length ids + String.fromCharCode(0x00) + cert.subject.uniqueId + ) + ]) + ) + } + + if (cert.extensions.length > 0) { + // extensions (optional) + tbs.value.push(pki.certificateExtensionsToAsn1(cert.extensions)) + } + + return tbs +} + +// function taken form selfsigned +// a hexString is considered negative if it's most significant bit is 1 +// because serial numbers use ones' complement notation +// this RFC in section 4.1.2.2 requires serial numbers to be positive +// http://www.ietf.org/rfc/rfc5280.txt +/** + * @param {string} hexString + * @returns + */ +function toPositiveHex(hexString) { + let mostSiginficativeHexAsInt = parseInt(hexString[0], 16) + if (mostSiginficativeHexAsInt < 8) { + return hexString + } + + mostSiginficativeHexAsInt -= 8 + return mostSiginficativeHexAsInt.toString() + hexString.substring(1) +} + +// the next is an edit of the selfsigned function reduced to the function necessary for webtransport +/** + * @typedef {object} Certificate + * @property {string} public + * @property {string} private + * @property {string} cert + * @property {Uint8Array} hash + * @property {string} fingerprint + * + * @param {*} attrs + * @param {*} options + * @returns {Promise} + */ +export async function generateWebTransportCertificate(attrs, options) { + try { + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + true, + ['sign', 'verify'] + ) + + const cert = pki.createCertificate() + + cert.serialNumber = toPositiveHex( + forge.util.bytesToHex(forge.random.getBytesSync(9)) + ) // the serial number can be decimal or hex (if preceded by 0x) + cert.validity.notBefore = new Date() + cert.validity.notAfter = new Date() + cert.validity.notAfter.setDate( + cert.validity.notBefore.getDate() + (options.days || 14) + ) // per spec only 14 days allowed + + cert.setSubject(attrs) + cert.setIssuer(attrs) + + const privateKey = crypto.subtle.exportKey('pkcs8', keyPair.privateKey) + const publicKey = (cert.publicKey = await crypto.subtle.exportKey( + 'spki', + keyPair.publicKey + )) + + cert.setExtensions( + options.extensions || [ + { + name: 'basicConstraints', + cA: true + }, + { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true + }, + { + name: 'subjectAltName', + altNames: [ + { + type: 6, // URI + value: 'http://example.org/webid#me' + } + ] + } + ] + ) + + // to signing + // patch oids object + oids['1.2.840.10045.4.3.2'] = 'ecdsa-with-sha256' + oids['ecdsa-with-sha256'] = '1.2.840.10045.4.3.2' + + cert.siginfo.algorithmOid = cert.signatureOid = '1.2.840.10045.4.3.2' // 'ecdsa-with-sha256' + + cert.tbsCertificate = getTBSCertificate(cert) + const encoded = Buffer.from( + asn1.toDer(cert.tbsCertificate).getBytes(), + 'binary' + ) + cert.md = crypto.subtle.digest('SHA-256', encoded) + cert.signature = crypto.subtle.sign( + { + name: 'ECDSA', + hash: { name: 'SHA-256' } + }, + keyPair.privateKey, + encoded + ) + cert.md = await cert.md + cert.signature = await cert.signature + + const pemcert = pki.certificateToPem(cert) + + const x509cert = new X509Certificate(pemcert) + + const certhash = Buffer.from( + x509cert.fingerprint256.split(':').map((el) => parseInt(el, 16)) + ) + + const pem = { + private: forge.pem.encode({ + type: 'PRIVATE KEY', + body: new forge.util.ByteBuffer(await privateKey).getBytes() + }), + public: forge.pem.encode({ + type: 'PUBLIC KEY', + body: new forge.util.ByteBuffer(publicKey).getBytes() + }), + cert: pemcert, + hash: certhash, + fingerprint: x509cert.fingerprint256 + } + + return pem + } catch (error) { + console.log('error in generate certificate', error) + return null + } +} diff --git a/packages/engine.io/test/webtransport.mjs b/packages/engine.io/test/webtransport.mjs new file mode 100644 index 00000000..10948ba2 --- /dev/null +++ b/packages/engine.io/test/webtransport.mjs @@ -0,0 +1,456 @@ +import * as eio from "../build/server.js"; +import { Http3Server, WebTransport } from "@fails-components/webtransport"; +import { Http3EventLoop } from "@fails-components/webtransport/lib/event-loop.js"; +import expect from "expect.js"; +import request from "superagent"; +import { createServer } from "http"; +import { generateWebTransportCertificate } from "./util.mjs"; + +const TEXT_ENCODER = new TextEncoder(); +const TEXT_DECODER = new TextDecoder(); + +function success(engine, h3server, done) { + engine.close(); + h3server.stopServer(); + done(); +} + +function createPartialDone(done, count) { + let i = 0; + return () => { + if (++i === count) { + done(); + } else if (i > count) { + done(new Error(`partialDone() called too many times: ${i} > ${count}`)); + } + }; +} + +async function setupServer(opts, cb) { + const certificate = await generateWebTransportCertificate( + [{ shortName: "CN", value: "localhost" }], + { + days: 14, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements) + } + ); + + const engine = new eio.Server(opts); + + const h3Server = new Http3Server({ + port: 0, // random port + host: "0.0.0.0", + secret: "changeit", + cert: certificate.cert, + privKey: certificate.private, + }); + + (async () => { + try { + const stream = await h3Server.sessionStream("/engine.io/"); + const sessionReader = stream.getReader(); + + while (true) { + const { done, value } = await sessionReader.read(); + if (done) { + break; + } + engine.onWebTransportSession(value); + } + } catch (ex) { + console.error("Server error", ex); + } + })(); + + h3Server.startServer(); + h3Server.onServerListening = () => cb({ engine, h3Server, certificate }); +} + +function setup(opts, cb) { + setupServer(opts, async ({ engine, h3Server, certificate }) => { + const client = new WebTransport( + `https://127.0.0.1:${h3Server.port}/engine.io/`, + { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: certificate.hash, + }, + ], + } + ); + + await client.ready; + + const stream = await client.createBidirectionalStream(); + const reader = stream.readable.getReader(); + const writer = stream.writable.getWriter(); + + engine.on("connection", async (socket) => { + await reader.read(); // header + await reader.read(); // payload (handshake) + + cb({ engine, h3Server, socket, client, stream, reader, writer }); + }); + + await writer.write(Uint8Array.of(1)); + await writer.write(TEXT_ENCODER.encode("0")); + }); +} + +describe("WebTransport", () => { + after(() => { + Http3EventLoop.globalLoop.shutdownEventLoop(); // manually shutdown the event loop, instead of waiting 20s + }); + + it("should allow to connect with WebTransport directly", (done) => { + setupServer({}, async ({ engine, h3Server, certificate }) => { + const partialDone = createPartialDone( + () => success(engine, h3Server, done), + 2 + ); + + engine.on("connection", (socket) => { + expect(socket.transport.name).to.eql("webtransport"); + partialDone(); + }); + + const client = new WebTransport( + `https://127.0.0.1:${h3Server.port}/engine.io/`, + { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: certificate.hash, + }, + ], + } + ); + + await client.ready; + + const stream = await client.createBidirectionalStream(); + const reader = stream.readable.getReader(); + const writer = stream.writable.getWriter(); + + (async function read() { + const header = await reader.read(); + + expect(header.value).to.eql(Uint8Array.of(107)); + + const { value } = await reader.read(); + + const handshake = TEXT_DECODER.decode(value); + expect(handshake.startsWith("0{")).to.be(true); + + partialDone(); + })(); + + writer.write(Uint8Array.of(1)); + writer.write(TEXT_ENCODER.encode("0")); + }); + }); + + it("should allow to upgrade to WebTransport", (done) => { + setupServer( + { + transports: ["polling", "websocket", "webtransport"], + }, + async ({ engine, h3Server, certificate }) => { + const httpServer = createServer(); + engine.attach(httpServer); + httpServer.listen(h3Server.port); + + const partialDone = createPartialDone(() => { + httpServer.close(); + success(engine, h3Server, done); + }, 2); + + engine.on("connection", (socket) => { + socket.on("upgrade", (transport) => { + expect(transport.name).to.eql("webtransport"); + partialDone(); + }); + }); + + request(`http://localhost:${h3Server.port}/engine.io/`) + .query({ EIO: 4, transport: "polling" }) + .end(async (_, res) => { + const payload = JSON.parse(res.text.substring(1)); + + expect(payload.upgrades).to.eql(["websocket", "webtransport"]); + + const client = new WebTransport( + `https://127.0.0.1:${h3Server.port}/engine.io/`, + { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: certificate.hash, + }, + ], + } + ); + + await client.ready; + + const stream = await client.createBidirectionalStream(); + const reader = stream.readable.getReader(); + const writer = stream.writable.getWriter(); + + (async function read() { + const header = await reader.read(); + + expect(header.value).to.eql(Uint8Array.of(6)); + + const { done, value } = await reader.read(); + + if (done) { + return; + } + + const probeValue = TEXT_DECODER.decode(value); + expect(probeValue).to.eql("3probe"); + + partialDone(); + })(); + + await writer.write(Uint8Array.of(31)); + await writer.write( + TEXT_ENCODER.encode(`0{"sid":"${payload.sid}"}`) + ); + await writer.write(Uint8Array.of(6)); + await writer.write(TEXT_ENCODER.encode(`2probe`)); + await writer.write(Uint8Array.of(1)); + await writer.write(TEXT_ENCODER.encode(`5`)); + }); + } + ); + }); + + it("should close a connection that fails to open a bidirectional stream", (done) => { + setupServer( + { + upgradeTimeout: 50, + }, + async ({ engine, h3Server, certificate }) => { + const client = new WebTransport( + `https://127.0.0.1:${h3Server.port}/engine.io/`, + { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: certificate.hash, + }, + ], + } + ); + + await client.ready; + + client.closed.then(() => { + success(engine, h3Server, done); + }); + } + ); + }); + + it("should close a connection that sends an invalid handshake", (done) => { + setupServer( + { + upgradeTimeout: 50, + }, + async ({ engine, h3Server, certificate }) => { + const client = new WebTransport( + `https://127.0.0.1:${h3Server.port}/engine.io/`, + { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: certificate.hash, + }, + ], + } + ); + + await client.ready; + const stream = await client.createBidirectionalStream(); + const writer = stream.writable.getWriter(); + + await writer.write(Uint8Array.of(1, 2, 3)); + + client.closed.then(() => { + success(engine, h3Server, done); + }); + } + ); + }); + + it("should send ping/pong packets", (done) => { + setup( + { + pingInterval: 20, + }, + async ({ engine, h3Server, reader, writer }) => { + for (let i = 0; i < 5; i++) { + const header = await reader.read(); + expect(header.value).to.eql(Uint8Array.of(1)); + + const packet = await reader.read(); + const value = TEXT_DECODER.decode(packet.value); + expect(value).to.eql("2"); + + writer.write(Uint8Array.of(1)); + writer.write(TEXT_ENCODER.encode("3")); + } + + success(engine, h3Server, done); + } + ); + }); + + it("should close on ping timeout", (done) => { + setup( + { + pingInterval: 20, + pingTimeout: 30, + }, + async ({ engine, h3Server, socket, client }) => { + const partialDone = createPartialDone(done, 2); + socket.on("close", (reason) => { + expect(reason).to.eql("ping timeout"); + partialDone(); + }); + + client.closed.then(() => success(engine, h3Server, partialDone)); + } + ); + }); + + it("should handle connections closed by the server", (done) => { + setup({}, async ({ engine, h3Server, socket, client }) => { + client.closed.then(() => success(engine, h3Server, done)); + + socket.close(); + }); + }); + + it("should handle connections closed by the client", (done) => { + setup({}, async ({ engine, h3Server, socket, client }) => { + socket.on("close", (reason) => { + expect(reason).to.eql("transport close"); + success(engine, h3Server, done); + }); + + client.close(); + }); + }); + + it("should send some plaintext data (client to server)", (done) => { + setup({}, async ({ engine, h3Server, socket, writer }) => { + socket.on("data", (data) => { + expect(data).to.eql("hello"); + + success(engine, h3Server, done); + }); + + writer.write(Uint8Array.of(6)); + writer.write(TEXT_ENCODER.encode("4hello")); + }); + }); + + it("should send some plaintext data (server to client)", (done) => { + setup({}, async ({ engine, h3Server, socket, reader }) => { + socket.send("hello"); + + const header = await reader.read(); + expect(header.value).to.eql(Uint8Array.of(6)); + + const { value } = await reader.read(); + const decoded = TEXT_DECODER.decode(value); + expect(decoded).to.eql("4hello"); + + success(engine, h3Server, done); + }); + }); + + it("should invoke send callbacks (server to client)", (done) => { + setup({}, async ({ engine, h3Server, socket, reader }) => { + const messageCount = 4; + let receivedCallbacks = 0; + + for (let i = 0; i < messageCount; i++) { + socket.send("hello", () => { + if (++receivedCallbacks === messageCount) { + success(engine, h3Server, done); + } + }); + } + }); + }); + + it("should send some binary data (client to server)", (done) => { + setup({}, async ({ engine, h3Server, socket, writer }) => { + socket.on("data", (data) => { + expect(Buffer.isBuffer(data)).to.be(true); + expect(data).to.eql(Buffer.of(1, 2, 3)); + + success(engine, h3Server, done); + }); + + writer.write(Uint8Array.of(131)); + writer.write(Uint8Array.of(1, 2, 3)); + }); + }); + + it("should send some binary data (server to client)", (done) => { + setup({}, async ({ engine, h3Server, socket, reader }) => { + socket.send(Buffer.of(1, 2, 3)); + + const header = await reader.read(); + expect(header.value).to.eql(Uint8Array.of(131)); + + const { value } = await reader.read(); + expect(value).to.eql(Uint8Array.of(1, 2, 3)); + + success(engine, h3Server, done); + }); + }); + + it("should send some big binary data (client to server)", (done) => { + setup({}, async ({ engine, h3Server, socket, writer }) => { + const payload = Buffer.allocUnsafe(1e6); + + socket.on("data", (data) => { + expect(Buffer.isBuffer(data)).to.be(true); + expect(data).to.eql(payload); + + success(engine, h3Server, done); + }); + + writer.write(Uint8Array.of(255, 0, 0, 0, 0, 0, 15, 66, 64)); + writer.write(payload); + }); + }); + + it("should send some big binary data (server to client)", (done) => { + setup({}, async ({ engine, h3Server, socket, reader }) => { + const payload = Buffer.allocUnsafe(1e6); + + socket.send(payload); + + const header = await reader.read(); + expect(header.value).to.eql( + Uint8Array.of(255, 0, 0, 0, 0, 0, 15, 66, 64) + ); + + const chunk1 = await reader.read(); + // the size of the chunk is implementation-specific (maxDatagramSize) + expect(chunk1.value).to.eql(payload.slice(0, 1228)); + + const chunk2 = await reader.read(); + expect(chunk2.value).to.eql(payload.slice(1228, 2456)); + + success(engine, h3Server, done); + }); + }); +}); diff --git a/packages/engine.io/tsconfig.json b/packages/engine.io/tsconfig.json new file mode 100644 index 00000000..72ed6e2d --- /dev/null +++ b/packages/engine.io/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "build/", + "target": "es2018", // Node.js 10 (https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping) + "module": "commonjs", + "declaration": true + }, + "include": [ + "./lib/**/*" + ] +} diff --git a/packages/engine.io/wrapper.mjs b/packages/engine.io/wrapper.mjs new file mode 100644 index 00000000..d0b2debf --- /dev/null +++ b/packages/engine.io/wrapper.mjs @@ -0,0 +1,10 @@ +export { + Server, + Socket, + Transport, + transports, + listen, + attach, + parser, + protocol, +} from "./build/engine.io.js";