diff --git a/.eslintrc.json b/.eslintrc.json index 6d3c020b6..082c0cb59 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,10 +1,4 @@ { - "extends": "standard", - "parser": "babel-eslint", - "rules": { - "yoda": 0, - "semi": [2, "always"], - "no-extra-semi": 2, - "semi-spacing": [2, { "before": false, "after": true }] - } + "extends": "prettier", + "parser": "babel-eslint" } diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 196c48004..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,25 +0,0 @@ - -*Note*: for support questions, please use one of these channels: [stackoverflow](http://stackoverflow.com/questions/tagged/engine.io) or [slack](https://socketio.slack.com) - -### You want to: - -* [x] report a *bug* -* [ ] request a *feature* - -### Current behaviour - - -### Steps to reproduce (if the current behaviour is a bug) - - -### Expected behaviour - - -### Setup -- OS: -- browser: -- engine.io version: - -### Other information (e.g. stacktraces, related issues, suggestions how to fix) - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..7deb7968f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,66 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** + +Please fill the following code example: + +Engine.IO server version: `x.y.z` + +*Server* + +```js +const engine = require("engine.io"); +const server = engine.listen(3000, {}); + +server.on("connection", (socket) => { + console.log("connection"); + + socket.on("message", (data) => { + console.log("data", data); + }); + + socket.on("close", () => { + console.log("close"); + }); +}); +``` + +Engine.IO client version: `x.y.z` + +*Client* + +```js +const socket = require("engine.io-client")("ws://localhost:3000"); + +socket.on("open", () => { + console.log("open"); + + socket.on("message", (data) => { + console.log("data", data); + }); + + socket.on("close", () => { + console.log("close"); + }); +}); +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Platform:** + - Device: [e.g. Samsung S8] + - OS: [e.g. Android 9.2] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..fe32cb299 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a Question + url: https://github.com/socketio/socket.io/discussions/new?category=q-a + about: Ask the community for help diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..36014cde5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..62ebbbf17 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * 0' + +permissions: + contents: read + +jobs: + test-node: + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + matrix: + node-version: + - 10 + - 18 + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Run tests with µWebSockets.js + run: npm run test:uws + if: ${{ matrix.node-version == '18' }} diff --git a/.gitignore b/.gitignore index 93f136199..fef838544 100755 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules npm-debug.log +build/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..935d77928 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +lib/parser-v3/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 968ece7d4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -sudo: false -language: node_js -node_js: - - "4" - - "6" - - "8" - - "9" -git: - depth: 1 -notifications: - irc: "irc.freenode.org#socket.io" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..3ec49ec4b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,475 @@ +# History + +## 2022 + +- [6.2.1](#621-2022-11-20) (Nov 2022) +- [3.6.0](#360-2022-06-06) (Jun 2022) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) +- [6.2.0](#620-2022-04-17) (Apr 2022) +- [6.1.3](#613-2022-02-23) (Feb 2022) +- [6.1.2](#612-2022-01-18) (Jan 2022) +- [6.1.1](#611-2022-01-11) (Jan 2022) + +## 2021 + +- [6.1.0](#610-2021-11-08) (Nov 2021) +- [6.0.1](#601-2021-11-06) (Nov 2021) +- [**6.0.0**](#600-2021-10-08) (Oct 2021) +- [5.2.0](#520-2021-08-29) (Aug 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) (Mar 2021) +- [4.1.1](#411-2021-02-02) (Feb 2021) +- [4.1.0](#410-2021-01-14) (Jan 2021) +- [4.0.6](#406-2021-01-04) (Jan 2021) + +## 2020 + +- [3.5.0](#350-2020-12-30) (Dec 2020) (from the [3.x](https://github.com/socketio/engine.io/tree/3.x) branch) +- [4.0.5](#405-2020-12-07) (Dec 2020) +- [4.0.4](#404-2020-11-17) (Nov 2020) +- [4.0.3](#403-2020-11-17) (Nov 2020) +- [4.0.2](#402-2020-11-09) (Nov 2020) +- [4.0.1](#401-2020-10-21) (Oct 2020) +- [**4.0.0**](#400-2020-09-10) (Sep 2020) +- [3.4.2](#342-2020-06-04) (Jun 2020) +- [3.4.1](#341-2020-04-17) (Apr 2020) + + + +# Release notes + +## [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)) + + + +# [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/README.md b/README.md index 1bc83d1ab..da038e19a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Engine.IO: the realtime engine -[![Build Status](https://travis-ci.org/socketio/engine.io.svg?branch=master)](http://travis-ci.org/socketio/engine.io) +[![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 @@ -15,43 +15,44 @@ cross-browser/cross-device bi-directional communication layer for #### (A) Listening on a port ```js -var engine = require('engine.io'); -var server = engine.listen(80); +const engine = require('engine.io'); +const server = engine.listen(80); -server.on('connection', function(socket){ +server.on('connection', socket => { socket.send('utf 8 string'); - socket.send(new Buffer([0, 1, 2, 3, 4, 5])); // binary data + socket.send(Buffer.from([0, 1, 2, 3, 4, 5])); // binary data }); ``` #### (B) Intercepting requests for a http.Server ```js -var engine = require('engine.io'); -var http = require('http').createServer().listen(3000); -var server = engine.attach(http); +const engine = require('engine.io'); +const http = require('http').createServer().listen(3000); +const server = engine.attach(http); -server.on('connection', function (socket) { - socket.on('message', function(data){ }); - socket.on('close', function(){ }); +server.on('connection', socket => { + socket.on('message', data => { }); + socket.on('close', () => { }); }); ``` #### (C) Passing in requests ```js -var engine = require('engine.io'); -var server = new engine.Server(); +const engine = require('engine.io'); +const server = new engine.Server(); -server.on('connection', function(socket){ +server.on('connection', socket => { socket.send('hi'); }); // … -httpServer.on('upgrade', function(req, socket, head){ +httpServer.on('upgrade', (req, socket, head) => { server.handleUpgrade(req, socket, head); }); -httpServer.on('request', function(req, res){ + +httpServer.on('request', (req, res) => { server.handleRequest(req, res); }); ``` @@ -61,16 +62,16 @@ httpServer.on('request', function(req, res){ ```html ``` For more information on the client refer to the -[engine-client](http://github.com/learnboost/engine.io-client) repository. +[engine-client](http://github.com/socketio/engine.io-client) repository. ## What features does it have? @@ -86,7 +87,6 @@ For more information on the client refer to the - **Future proof** - **100% Node.JS core style** - No API sugar (left for higher level projects) - - Written in readable vanilla JavaScript ## API @@ -131,21 +131,21 @@ These are exposed by `require('engine.io')`: The following are identical ways to instantiate a server and then attach it. ```js -var httpServer; // previously created with `http.createServer();` from node.js api. +const httpServer; // previously created with `http.createServer();` from node.js api. // create a server first, and then attach -var eioServer = require('engine.io').Server(); +const eioServer = require('engine.io').Server(); eioServer.attach(httpServer); // or call the module as a function to get `Server` -var eioServer = require('engine.io')(); +const eioServer = require('engine.io')(); eioServer.attach(httpServer); // immediately attach -var eioServer = require('engine.io')(httpServer); +const eioServer = require('engine.io')(httpServer); // with custom options -var eioServer = require('engine.io')(httpServer, { +const eioServer = require('engine.io')(httpServer, { maxHttpBufferSize: 1e3 }); ``` @@ -163,8 +163,8 @@ var eioServer = require('engine.io')(httpServer, { - **Returns** `Server` ```js -var engine = require('engine.io'); -var server = engine.listen(3000, { +const engine = require('engine.io'); +const server = engine.listen(3000, { pingTimeout: 2000, pingInterval: 10000 }); @@ -184,10 +184,10 @@ server.on('connection', /* ... */); - **Returns** `Server` a new Server instance. ```js -var engine = require('engine.io'); -var httpServer = require('http').createServer().listen(3000); -var server = engine.attach(httpServer, { - wsEngine: 'uws' // requires having uws as dependency +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', /* ... */); @@ -204,6 +204,37 @@ The main server/manager. _Inherits from EventEmitter_. - **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 @@ -221,13 +252,13 @@ to a single process. - `Object`: optional, options object - **Options** - `pingTimeout` (`Number`): how many ms without a pong packet to - consider the connection closed (`5000`) + 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 `10E7`. + 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 @@ -239,20 +270,19 @@ to a single process. - `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 `false` to disable. (`true`) + (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` (`String|Boolean`): name of the HTTP cookie that + - `cookie` (`Object|Boolean`): configuration of the cookie that contains the client sid to send as part of handshake response - headers. Set to `false` to not send one. (`io`) - - `cookiePath` (`String|Boolean`): path of the above `cookie` - option. If false, no path will be sent, which means browsers will only send the cookie on the engine.io attached path (`/engine.io`). - Set false to not save io cookie on all requests. (`/`) - - `cookieHttpOnly` (`Boolean`): If `true` HttpOnly io cookie cannot be accessed by client-side APIs, such as JavaScript. (`true`) _This option has no effect if `cookie` or `cookiePath` is set to `false`._ - - `wsEngine` (`String`): 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 `uws` module. + 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 @@ -280,7 +310,6 @@ to a single process. - `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`) - - `handlePreflightRequest` (`Boolean|Function`): whether to let engine.io handle the OPTIONS requests. You can also pass a custom function to handle the requests (`true`) - `generateId` - Generate a socket id. - Overwrite this method to generate your custom socket id. @@ -321,10 +350,12 @@ A representation of a client. _Inherits from EventEmitter_. - `type`: packet type - `data`: packet data (if type is message) - `packetCreate` - - Called before a socket sends a packet (`message`, `pong`) + - 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 @@ -534,9 +565,9 @@ has you covered. ### Can I implement `Engine` in other languages? -Absolutely. The [engine.io-protocol](https://github.com/LearnBoost/engine.io-protocol) -repository contains the most up to date description of the specification -at all times, and the parser implementation in JavaScript. +Absolutely. The [engine.io-protocol](https://github.com/socketio/engine.io-protocol) +repository contains the most up-to-date description of the specification +at all times. ## License diff --git a/examples/latency/index.html b/examples/latency/index.html index a89d95d2a..266548e06 100644 --- a/examples/latency/index.html +++ b/examples/latency/index.html @@ -9,6 +9,7 @@

EIO Latency

(connecting)

+ diff --git a/examples/latency/index.js b/examples/latency/index.js index 2622a1a13..db7ec3d42 100644 --- a/examples/latency/index.js +++ b/examples/latency/index.js @@ -3,28 +3,32 @@ * Module dependencies. */ -var express = require('express'); -var app = express(); -var server = require('http').createServer(app); -var enchilada = require('enchilada'); -var io = require('engine.io').attach(server); +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('/', function (req, res, next) { - res.sendfile('index.html'); +app.get('/', (req, res) => { + res.sendFile(__dirname + '/index.html'); }); -io.on('connection', function (socket) { - socket.on('message', function (v) { +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'); }); }); -var port = process.env.PORT || 3000; -server.listen(port, function () { +const port = process.env.PORT || 3000; +server.listen(port, () => { console.log('\x1B[96mlistening on localhost:' + port + ' \x1B[39m'); }); diff --git a/examples/latency/package-lock.json b/examples/latency/package-lock.json new file mode 100644 index 000000000..d88dfdbe6 --- /dev/null +++ b/examples/latency/package-lock.json @@ -0,0 +1,1548 @@ +{ + "name": "eio-latency", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "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.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" + } + }, + "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": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "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.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "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": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "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.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "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=" + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "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.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "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": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "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": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "engine.io": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.2.tgz", + "integrity": "sha512-t5z6zjXuVLhXDMiFJPYsPOWEER8B0tIsD3ETgw19S1yg9zryvUfY3Vhtk3Gf4sihw/bQGIqQ//gjvVlu+Ca0bQ==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.0", + "ws": "~7.4.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "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" + } + }, + "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-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==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "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": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "events": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/events/-/events-1.0.2.tgz", + "integrity": "sha1-dYSdz+k9EPsFfDAFWv29UdBqjiQ=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "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": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "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.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "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": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "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" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==" + }, + "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": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "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": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + }, + "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" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "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": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "process": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.7.0.tgz", + "integrity": "sha1-xSIIFho0rfOBI0SuhdPmFQRpOJ0=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "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.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "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.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "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.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "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.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.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" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "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=" + }, + "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": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "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.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "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": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "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": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "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/examples/latency/package.json b/examples/latency/package.json index c9e60ff1f..7f2ea6b5d 100644 --- a/examples/latency/package.json +++ b/examples/latency/package.json @@ -2,10 +2,10 @@ "name": "eio-latency", "version": "0.1.0", "dependencies": { - "express": "3.3.5", "enchilada": "0.13.0", - "engine.io": "1.6.0", - "engine.io-client": "1.6.0", + "engine.io": "^4.1.2", + "engine.io-client": "^4.1.4", + "express": "^4.17.1", "smoothie": "1.19.0" } } diff --git a/examples/latency/public/index.js b/examples/latency/public/index.js index d64f8efd2..ea74058e2 100644 --- a/examples/latency/public/index.js +++ b/examples/latency/public/index.js @@ -3,9 +3,8 @@ * Module dependencies. */ -var SmoothieChart = require('smoothie').SmoothieChart; -var TimeSeries = require('smoothie').TimeSeries; -var eio = require('engine.io-client'); +const SmoothieChart = require('smoothie').SmoothieChart; +const TimeSeries = require('smoothie').TimeSeries; // helper @@ -13,8 +12,8 @@ function $ (id) { return document.getElementById(id); } // chart -var smoothie; -var time; +let smoothie; +let time; function render () { if (smoothie) smoothie.stop(); @@ -30,26 +29,29 @@ function render () { } // socket -var socket = new eio.Socket(); -var last; +const socket = new eio.Socket(); +let last; function send () { last = new Date(); socket.send('ping'); $('transport').innerHTML = socket.transport.name; } -socket.on('open', function () { + +socket.on('open', () => { if ($('chart').getContext) { render(); window.onresize = render; } send(); }); -socket.on('close', function () { + +socket.on('close', () => { if (smoothie) smoothie.stop(); $('transport').innerHTML = '(disconnected)'; }); -socket.on('message', function () { - var latency = new Date() - last; + +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/lib/engine.io.js b/lib/engine.io.js deleted file mode 100644 index 2a211c2b2..000000000 --- a/lib/engine.io.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Module dependencies. - */ - -var http = require('http'); - -/** - * Invoking the library as a function delegates to attach if the first argument - * is an `http.Server`. - * - * If there are no arguments or the first argument is an options object, then - * a new Server instance is returned. - * - * @param {http.Server} server (if specified, will be attached to by the new Server instance) - * @param {Object} options - * @return {Server} engine server - * @api public - */ - -exports = module.exports = function () { - // backwards compatible use as `.attach` - // if first argument is an http server - if (arguments.length && arguments[0] instanceof http.Server) { - return attach.apply(this, arguments); - } - - // if first argument is not an http server, then just make a regular eio server - return exports.Server.apply(null, arguments); -}; - -/** - * Protocol revision number. - * - * @api public - */ - -exports.protocol = 1; - -/** - * Expose Server constructor. - * - * @api public - */ - -exports.Server = require('./server'); - -/** - * Expose Socket constructor. - * - * @api public - */ - -exports.Socket = require('./socket'); - -/** - * Expose Transport constructor. - * - * @api public - */ - -exports.Transport = require('./transport'); - -/** - * Expose mutable list of available transports. - * - * @api public - */ - -exports.transports = require('./transports'); - -/** - * Exports parser. - * - * @api public - */ - -exports.parser = require('engine.io-parser'); - -/** - * Creates an http.Server exclusively used for WS upgrades. - * - * @param {Number} port - * @param {Function} callback - * @param {Object} options - * @return {Server} websocket.io server - * @api public - */ - -exports.listen = listen; - -function listen (port, options, fn) { - if ('function' === typeof options) { - fn = options; - options = {}; - } - - var server = http.createServer(function (req, res) { - res.writeHead(501); - res.end('Not Implemented'); - }); - - // create engine server - var engine = exports.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 - * @api public - */ - -exports.attach = attach; - -function attach (server, options) { - var engine = new exports.Server(options); - engine.attach(server, options); - return engine; -} diff --git a/lib/engine.io.ts b/lib/engine.io.ts new file mode 100644 index 000000000..b57ed6390 --- /dev/null +++ b/lib/engine.io.ts @@ -0,0 +1,56 @@ +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 { AttachOptions, ServerOptions } 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 + * @api public + */ + +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 + * @api public + */ + +function attach(server, options: AttachOptions & ServerOptions) { + const engine = new Server(options); + engine.attach(server, options); + return engine; +} diff --git a/lib/parser-v3/index.ts b/lib/parser-v3/index.ts new file mode 100644 index 000000000..367ae1376 --- /dev/null +++ b/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/lib/parser-v3/utf8.ts b/lib/parser-v3/utf8.ts new file mode 100644 index 000000000..b878740ff --- /dev/null +++ b/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/lib/server.js b/lib/server.js deleted file mode 100644 index 3884e9ce1..000000000 --- a/lib/server.js +++ /dev/null @@ -1,575 +0,0 @@ - -/** - * Module dependencies. - */ - -var qs = require('querystring'); -var parse = require('url').parse; -var base64id = require('base64id'); -var transports = require('./transports'); -var EventEmitter = require('events').EventEmitter; -var Socket = require('./socket'); -var util = require('util'); -var debug = require('debug')('engine'); -var cookieMod = require('cookie'); - -/** - * Module exports. - */ - -module.exports = Server; - -/** - * Server constructor. - * - * @param {Object} options - * @api public - */ - -function Server (opts) { - if (!(this instanceof Server)) { - return new Server(opts); - } - - this.clients = {}; - this.clientsCount = 0; - - opts = opts || {}; - - this.wsEngine = opts.wsEngine || process.env.EIO_WS_ENGINE || 'ws'; - this.pingTimeout = opts.pingTimeout || 5000; - this.pingInterval = opts.pingInterval || 25000; - this.upgradeTimeout = opts.upgradeTimeout || 10000; - this.maxHttpBufferSize = opts.maxHttpBufferSize || 10E7; - this.transports = opts.transports || Object.keys(transports); - this.allowUpgrades = false !== opts.allowUpgrades; - this.allowRequest = opts.allowRequest; - this.cookie = false !== opts.cookie ? (opts.cookie || 'io') : false; - this.cookiePath = false !== opts.cookiePath ? (opts.cookiePath || '/') : false; - this.cookieHttpOnly = false !== opts.cookieHttpOnly; - this.perMessageDeflate = false !== opts.perMessageDeflate ? (opts.perMessageDeflate || true) : false; - this.httpCompression = false !== opts.httpCompression ? (opts.httpCompression || {}) : false; - this.initialPacket = opts.initialPacket; - - var self = this; - - // initialize compression options - ['perMessageDeflate', 'httpCompression'].forEach(function (type) { - var compression = self[type]; - if (true === compression) self[type] = compression = {}; - if (compression && null == compression.threshold) { - compression.threshold = 1024; - } - }); - - this.init(); -} - -/** - * Protocol errors mappings. - */ - -Server.errors = { - UNKNOWN_TRANSPORT: 0, - UNKNOWN_SID: 1, - BAD_HANDSHAKE_METHOD: 2, - BAD_REQUEST: 3, - FORBIDDEN: 4 -}; - -Server.errorMessages = { - 0: 'Transport unknown', - 1: 'Session ID unknown', - 2: 'Bad handshake method', - 3: 'Bad request', - 4: 'Forbidden' -}; - -/** - * Inherits from EventEmitter. - */ - -util.inherits(Server, EventEmitter); - -/** - * Initialize websocket server - * - * @api private - */ - -Server.prototype.init = function () { - if (!~this.transports.indexOf('websocket')) return; - - if (this.ws) this.ws.close(); - - var wsModule; - switch (this.wsEngine) { - case 'uws': wsModule = require('uws'); break; - case 'ws': wsModule = require('ws'); break; - default: throw new Error('unknown wsEngine'); - } - this.ws = new wsModule.Server({ - noServer: true, - clientTracking: false, - perMessageDeflate: this.perMessageDeflate, - maxPayload: this.maxHttpBufferSize - }); -}; - -/** - * Returns a list of available transports for upgrade given a certain transport. - * - * @return {Array} - * @api public - */ - -Server.prototype.upgrades = function (transport) { - if (!this.allowUpgrades) return []; - return transports[transport].upgradesTo || []; -}; - -/** - * Verifies a request. - * - * @param {http.IncomingMessage} - * @return {Boolean} whether the request is valid - * @api private - */ - -Server.prototype.verify = function (req, upgrade, fn) { - // transport check - var transport = req._query.transport; - if (!~this.transports.indexOf(transport)) { - debug('unknown transport "%s"', transport); - return fn(Server.errors.UNKNOWN_TRANSPORT, false); - } - - // 'Origin' header check - var isOriginInvalid = checkInvalidHeaderChar(req.headers.origin); - if (isOriginInvalid) { - req.headers.origin = null; - return fn(Server.errors.BAD_REQUEST, false); - } - - // sid check - var sid = req._query.sid; - if (sid) { - if (!this.clients.hasOwnProperty(sid)) { - return fn(Server.errors.UNKNOWN_SID, false); - } - if (!upgrade && this.clients[sid].transport.name !== transport) { - debug('bad request: unexpected transport without upgrade'); - return fn(Server.errors.BAD_REQUEST, false); - } - } else { - // handshake is GET only - if ('GET' !== req.method) return fn(Server.errors.BAD_HANDSHAKE_METHOD, false); - if (!this.allowRequest) return fn(null, true); - return this.allowRequest(req, fn); - } - - fn(null, true); -}; - -/** - * Prepares a request by processing the query string. - * - * @api private - */ - -Server.prototype.prepare = function (req) { - // 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) : {}; - } -}; - -/** - * Closes all clients. - * - * @api public - */ - -Server.prototype.close = function () { - debug('closing all open clients'); - for (var i in this.clients) { - if (this.clients.hasOwnProperty(i)) { - this.clients[i].close(true); - } - } - 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 - } - return this; -}; - -/** - * Handles an Engine.IO HTTP request. - * - * @param {http.IncomingMessage} request - * @param {http.ServerResponse|http.OutgoingMessage} response - * @api public - */ - -Server.prototype.handleRequest = function (req, res) { - debug('handling "%s" http request "%s"', req.method, req.url); - this.prepare(req); - req.res = res; - - var self = this; - this.verify(req, false, function (err, success) { - if (!success) { - sendErrorMessage(req, res, err); - return; - } - - if (req._query.sid) { - debug('setting new request for existing client'); - self.clients[req._query.sid].transport.onRequest(req); - } else { - self.handshake(req._query.transport, req); - } - }); -}; - -/** - * Sends an Engine.IO Error Message - * - * @param {http.ServerResponse} response - * @param {code} error code - * @api private - */ - -function sendErrorMessage (req, res, code) { - var headers = { 'Content-Type': 'application/json' }; - - var isForbidden = !Server.errorMessages.hasOwnProperty(code); - if (isForbidden) { - res.writeHead(403, headers); - res.end(JSON.stringify({ - code: Server.errors.FORBIDDEN, - message: code || Server.errorMessages[Server.errors.FORBIDDEN] - })); - return; - } - if (req.headers.origin) { - headers['Access-Control-Allow-Credentials'] = 'true'; - headers['Access-Control-Allow-Origin'] = req.headers.origin; - } else { - headers['Access-Control-Allow-Origin'] = '*'; - } - if (res !== undefined) { - res.writeHead(400, headers); - res.end(JSON.stringify({ - code: code, - message: Server.errorMessages[code] - })); - } -} - -/** - * generate a socket id. - * Overwrite this method to generate your custom socket id - * - * @param {Object} request object - * @api public - */ - -Server.prototype.generateId = function (req) { - return base64id.generateId(); -}; - -/** - * Handshakes a new client. - * - * @param {String} transport name - * @param {Object} request object - * @api private - */ - -Server.prototype.handshake = function (transportName, req) { - var id = this.generateId(req); - - debug('handshaking client "%s"', id); - - try { - var transport = new transports[transportName](req); - if ('polling' === transportName) { - transport.maxHttpBufferSize = this.maxHttpBufferSize; - transport.httpCompression = this.httpCompression; - } else if ('websocket' === transportName) { - transport.perMessageDeflate = this.perMessageDeflate; - } - - if (req._query && req._query.b64) { - transport.supportsBinary = false; - } else { - transport.supportsBinary = true; - } - } catch (e) { - sendErrorMessage(req, req.res, Server.errors.BAD_REQUEST); - return; - } - var socket = new Socket(id, this, transport, req); - var self = this; - - if (false !== this.cookie) { - transport.on('headers', function (headers) { - headers['Set-Cookie'] = cookieMod.serialize(self.cookie, id, - { - path: self.cookiePath, - httpOnly: self.cookiePath ? self.cookieHttpOnly : false - }); - }); - } - - transport.onRequest(req); - - this.clients[id] = socket; - this.clientsCount++; - - socket.once('close', function () { - delete self.clients[id]; - self.clientsCount--; - }); - - this.emit('connection', socket); -}; - -/** - * Handles an Engine.IO HTTP Upgrade. - * - * @api public - */ - -Server.prototype.handleUpgrade = function (req, socket, upgradeHead) { - this.prepare(req); - - var self = this; - this.verify(req, true, function (err, success) { - if (!success) { - abortConnection(socket, err); - return; - } - - var head = new Buffer(upgradeHead.length); // eslint-disable-line node/no-deprecated-api - upgradeHead.copy(head); - upgradeHead = null; - - // delegate to ws - self.ws.handleUpgrade(req, socket, head, function (conn) { - self.onWebSocket(req, conn); - }); - }); -}; - -/** - * Called upon a ws.io connection. - * - * @param {ws.Socket} websocket - * @api private - */ - -Server.prototype.onWebSocket = function (req, socket) { - socket.on('error', onUpgradeError); - - if (transports[req._query.transport] !== undefined && !transports[req._query.transport].prototype.handlesUpgrades) { - debug('transport doesnt handle upgraded requests'); - socket.close(); - return; - } - - // get client id - var id = req._query.sid; - - // keep a reference to the ws.Socket - req.websocket = socket; - - if (id) { - var client = this.clients[id]; - if (!client) { - debug('upgrade attempt for closed client'); - socket.close(); - } else if (client.upgrading) { - debug('transport has already been trying to upgrade'); - socket.close(); - } else if (client.upgraded) { - debug('transport had already been upgraded'); - socket.close(); - } else { - debug('upgrading existing transport'); - - // transport error handling takes over - socket.removeListener('error', onUpgradeError); - - var transport = new transports[req._query.transport](req); - if (req._query && req._query.b64) { - transport.supportsBinary = false; - } else { - transport.supportsBinary = true; - } - transport.perMessageDeflate = this.perMessageDeflate; - client.maybeUpgrade(transport); - } - } else { - // transport error handling takes over - socket.removeListener('error', onUpgradeError); - - this.handshake(req._query.transport, req); - } - - function onUpgradeError () { - debug('websocket error before upgrade'); - // socket.close() not needed - } -}; - -/** - * Captures upgrade requests for a http.Server. - * - * @param {http.Server} server - * @param {Object} options - * @api public - */ - -Server.prototype.attach = function (server, options) { - var self = this; - options = options || {}; - var path = (options.path || '/engine.io').replace(/\/$/, ''); - - var destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000; - - // normalize path - path += '/'; - - function check (req) { - if ('OPTIONS' === req.method && false === options.handlePreflightRequest) { - return false; - } - return path === req.url.substr(0, path.length); - } - - // cache and clean up listeners - var listeners = server.listeners('request').slice(0); - server.removeAllListeners('request'); - server.on('close', self.close.bind(self)); - server.on('listening', self.init.bind(self)); - - // add request handler - server.on('request', function (req, res) { - if (check(req)) { - debug('intercepting request for path "%s"', path); - if ('OPTIONS' === req.method && 'function' === typeof options.handlePreflightRequest) { - options.handlePreflightRequest.call(server, req, res); - } else { - self.handleRequest(req, res); - } - } else { - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].call(server, req, res); - } - } - }); - - if (~self.transports.indexOf('websocket')) { - server.on('upgrade', function (req, socket, head) { - if (check(req)) { - self.handleUpgrade(req, 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 () { - if (socket.writable && socket.bytesWritten <= 0) { - return socket.end(); - } - }, destroyUpgradeTimeout); - } - }); - } -}; - -/** - * Closes the connection - * - * @param {net.Socket} socket - * @param {code} error code - * @api private - */ - -function abortConnection (socket, code) { - if (socket.writable) { - var message = Server.errorMessages.hasOwnProperty(code) ? Server.errorMessages[code] : String(code || ''); - var 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. - **/ -var 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)]) - return true; - if (val.length < 2) - return false; - if (!validHdrChars[val.charCodeAt(1)]) - return true; - if (val.length < 3) - return false; - if (!validHdrChars[val.charCodeAt(2)]) - return true; - if (val.length < 4) - return false; - if (!validHdrChars[val.charCodeAt(3)]) - return true; - for (var i = 4; i < val.length; ++i) { - if (!validHdrChars[val.charCodeAt(i)]) - return true; - } - return false; -} diff --git a/lib/server.ts b/lib/server.ts new file mode 100644 index 000000000..c99f8ef86 --- /dev/null +++ b/lib/server.ts @@ -0,0 +1,817 @@ +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 { IncomingMessage, Server as HttpServer } from "http"; +import { CookieSerializeOptions } from "cookie"; +import { CorsOptions, CorsOptionsDelegate } from "cors"; + +const debug = debugModule("engine"); + +type Transport = "polling" | "websocket"; + +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; +} + +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 + * @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; +} + +export abstract class BaseServer extends EventEmitter { + public opts: ServerOptions; + + protected clients: any; + private clientsCount: number; + protected corsMiddleware: Function; + + /** + * Server constructor. + * + * @param {Object} opts - options + * @api public + */ + 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: Object.keys(transports), + 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.corsMiddleware = require("cors")(this.opts.cors); + } + + if (opts.perMessageDeflate) { + this.opts.perMessageDeflate = Object.assign( + { + threshold: 1024 + }, + opts.perMessageDeflate + ); + } + + this.init(); + } + + protected abstract init(); + + /** + * Returns a list of available transports for upgrade given a certain transport. + * + * @return {Array} + * @api public + */ + public upgrades(transport) { + if (!this.opts.allowUpgrades) return []; + return transports[transport].upgradesTo || []; + } + + /** + * Verifies a request. + * + * @param {http.IncomingMessage} + * @return {Boolean} whether the request is valid + * @api private + */ + protected verify(req, upgrade, fn) { + // transport check + const transport = req._query.transport; + if (!~this.opts.transports.indexOf(transport)) { + 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(); + } + + /** + * Closes all clients. + * + * @api public + */ + 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 {Object} request object + * @api public + */ + public generateId(req) { + return base64id.generateId(); + } + + /** + * Handshakes a new client. + * + * @param {String} transport name + * @param {Object} request object + * @param {Function} closeConnection + * + * @api protected + */ + protected async handshake(transportName, req, closeConnection) { + 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; + } + + if (req._query && req._query.b64) { + transport.supportsBinary = false; + } else { + transport.supportsBinary = true; + } + } 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; + } + + 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" + }; +} + +export class Server extends BaseServer { + public httpServer?: HttpServer; + private ws: any; + + /** + * Initialize websocket server + * + * @api 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 = {}; + + const isInitialRequest = !req._query.sid; + if (isInitialRequest) { + this.emit("initial_headers", additionalHeaders, req); + } + + this.emit("headers", additionalHeaders, req); + + 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. + * + * @api private + */ + private prepare(req) { + // 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) : {}; + } + } + + protected createTransport(transportName, req) { + return new transports[transportName](req); + } + + /** + * Handles an Engine.IO HTTP request. + * + * @param {http.IncomingMessage} request + * @param {http.ServerResponse|http.OutgoingMessage} response + * @api public + */ + public handleRequest(req, res) { + 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); + } + }; + + if (this.corsMiddleware) { + this.corsMiddleware.call(null, req, res, () => { + this.verify(req, false, callback); + }); + } else { + this.verify(req, false, callback); + } + } + + /** + * Handles an Engine.IO HTTP Upgrade. + * + * @api public + */ + public handleUpgrade(req, socket, upgradeHead) { + this.prepare(req); + + this.verify(req, true, (errorCode, errorContext) => { + if (errorCode) { + 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; + + // delegate to ws + this.ws.handleUpgrade(req, socket, head, websocket => { + this.onWebSocket(req, socket, websocket); + }); + }); + } + + /** + * Called upon a ws.io connection. + * + * @param {ws.Socket} websocket + * @api 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); + if (req._query && req._query.b64) { + transport.supportsBinary = false; + } else { + transport.supportsBinary = true; + } + 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 + * @api public + */ + public attach(server: HttpServer, options: AttachOptions = {}) { + let path = (options.path || "/engine.io").replace(/\/$/, ""); + + const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000; + + // normalize path + path += "/"; + + function check(req) { + 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, 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, 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 + * + * @api 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 + * + * @api private + */ + +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/lib/socket.js b/lib/socket.js deleted file mode 100644 index 177b25c69..000000000 --- a/lib/socket.js +++ /dev/null @@ -1,486 +0,0 @@ -/** - * Module dependencies. - */ - -var EventEmitter = require('events').EventEmitter; -var util = require('util'); -var debug = require('debug')('engine:socket'); - -/** - * Module exports. - */ - -module.exports = Socket; - -/** - * Client class (abstract). - * - * @api private - */ - -function Socket (id, server, transport, req) { - this.id = id; - this.server = server; - this.upgrading = false; - this.upgraded = false; - this.readyState = 'opening'; - this.writeBuffer = []; - this.packetsFn = []; - this.sentCallbackFn = []; - this.cleanupFn = []; - this.request = req; - - // Cache IP since it might not be in the req later - if (req.websocket && req.websocket._socket) { - this.remoteAddress = req.websocket._socket.remoteAddress; - } else { - this.remoteAddress = req.connection.remoteAddress; - } - - this.checkIntervalTimer = null; - this.upgradeTimeoutTimer = null; - this.pingTimeoutTimer = null; - - this.setTransport(transport); - this.onOpen(); -} - -/** - * Inherits from EventEmitter. - */ - -util.inherits(Socket, EventEmitter); - -/** - * Called upon transport considered open. - * - * @api private - */ - -Socket.prototype.onOpen = function () { - 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.pingInterval, - pingTimeout: this.server.pingTimeout - })); - - if (this.server.initialPacket) { - this.sendPacket('message', this.server.initialPacket); - } - - this.emit('open'); - this.setPingTimeout(); -}; - -/** - * Called upon transport packet. - * - * @param {Object} packet - * @api private - */ - -Socket.prototype.onPacket = function (packet) { - if ('open' === this.readyState) { - // export packet event - debug('packet'); - this.emit('packet', packet); - - // Reset ping timeout on any packet, incoming data is a good sign of - // other side's liveness - this.setPingTimeout(); - - switch (packet.type) { - case 'ping': - debug('got ping'); - this.sendPacket('pong'); - this.emit('heartbeat'); - break; - - case 'error': - this.onClose('parse error'); - break; - - case 'message': - this.emit('data', packet.data); - this.emit('message', packet.data); - break; - } - } else { - debug('packet received with closed socket'); - } -}; - -/** - * Called upon transport error. - * - * @param {Error} error object - * @api private - */ - -Socket.prototype.onError = function (err) { - debug('transport error'); - this.onClose('transport error', err); -}; - -/** - * Sets and resets ping timeout timer based on client pings. - * - * @api private - */ - -Socket.prototype.setPingTimeout = function () { - var self = this; - clearTimeout(self.pingTimeoutTimer); - self.pingTimeoutTimer = setTimeout(function () { - self.onClose('ping timeout'); - }, self.server.pingInterval + self.server.pingTimeout); -}; - -/** - * Attaches handlers for the given transport. - * - * @param {Transport} transport - * @api private - */ - -Socket.prototype.setTransport = function (transport) { - var onError = this.onError.bind(this); - var onPacket = this.onPacket.bind(this); - var flush = this.flush.bind(this); - var onClose = this.onClose.bind(this, 'transport close'); - - this.transport = transport; - this.transport.once('error', onError); - this.transport.on('packet', onPacket); - this.transport.on('drain', flush); - this.transport.once('close', onClose); - // this function will manage packet events (also message callbacks) - this.setupSendCallback(); - - this.cleanupFn.push(function () { - transport.removeListener('error', onError); - transport.removeListener('packet', onPacket); - transport.removeListener('drain', flush); - transport.removeListener('close', onClose); - }); -}; - -/** - * Upgrades socket to the given transport - * - * @param {Transport} transport - * @api private - */ - -Socket.prototype.maybeUpgrade = function (transport) { - debug('might upgrade socket transport from "%s" to "%s"' - , this.transport.name, transport.name); - - this.upgrading = true; - - var self = this; - - // set transport upgrade timer - self.upgradeTimeoutTimer = setTimeout(function () { - debug('client did not complete upgrade - closing transport'); - cleanup(); - if ('open' === transport.readyState) { - transport.close(); - } - }, this.server.upgradeTimeout); - - function onPacket (packet) { - if ('ping' === packet.type && 'probe' === packet.data) { - transport.send([{ type: 'pong', data: 'probe' }]); - self.emit('upgrading', transport); - clearInterval(self.checkIntervalTimer); - self.checkIntervalTimer = setInterval(check, 100); - } else if ('upgrade' === packet.type && self.readyState !== 'closed') { - debug('got upgrade packet - upgrading'); - cleanup(); - self.transport.discard(); - self.upgraded = true; - self.clearTransport(); - self.setTransport(transport); - self.emit('upgrade', transport); - self.setPingTimeout(); - self.flush(); - if (self.readyState === 'closing') { - transport.close(function () { - self.onClose('forced close'); - }); - } - } else { - cleanup(); - transport.close(); - } - } - - // we force a polling cycle to ensure a fast upgrade - function check () { - if ('polling' === self.transport.name && self.transport.writable) { - debug('writing a noop packet to polling for fast upgrade'); - self.transport.send([{ type: 'noop' }]); - } - } - - function cleanup () { - self.upgrading = false; - - clearInterval(self.checkIntervalTimer); - self.checkIntervalTimer = null; - - clearTimeout(self.upgradeTimeoutTimer); - self.upgradeTimeoutTimer = null; - - transport.removeListener('packet', onPacket); - transport.removeListener('close', onTransportClose); - transport.removeListener('error', onError); - self.removeListener('close', onClose); - } - - function onError (err) { - debug('client did not complete upgrade - %s', err); - cleanup(); - transport.close(); - transport = null; - } - - function onTransportClose () { - onError('transport closed'); - } - - function onClose () { - onError('socket closed'); - } - - transport.on('packet', onPacket); - transport.once('close', onTransportClose); - transport.once('error', onError); - - self.once('close', onClose); -}; - -/** - * Clears listeners and timers associated with current transport. - * - * @api private - */ - -Socket.prototype.clearTransport = function () { - var cleanup; - - var toCleanUp = this.cleanupFn.length; - - for (var 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` - */ - -Socket.prototype.onClose = function (reason, description) { - if ('closed' !== this.readyState) { - this.readyState = 'closed'; - clearTimeout(this.pingTimeoutTimer); - clearInterval(this.checkIntervalTimer); - this.checkIntervalTimer = null; - clearTimeout(this.upgradeTimeoutTimer); - var self = this; - // clean writeBuffer in next tick, so developers can still - // grab the writeBuffer on 'close' event - process.nextTick(function () { - self.writeBuffer = []; - }); - this.packetsFn = []; - this.sentCallbackFn = []; - this.clearTransport(); - this.emit('close', reason, description); - } -}; - -/** - * Setup and manage send callback - * - * @api private - */ - -Socket.prototype.setupSendCallback = function () { - var self = this; - this.transport.on('drain', onDrain); - - this.cleanupFn.push(function () { - self.transport.removeListener('drain', onDrain); - }); - - // the message was sent successfully, execute the callback - function onDrain () { - if (self.sentCallbackFn.length > 0) { - var seqFn = self.sentCallbackFn.splice(0, 1)[0]; - if ('function' === typeof seqFn) { - debug('executing send callback'); - seqFn(self.transport); - } else if (Array.isArray(seqFn)) { - debug('executing batch send callback'); - for (var l = seqFn.length, i = 0; i < l; i++) { - if ('function' === typeof seqFn[i]) { - seqFn[i](self.transport); - } - } - } - } - } -}; - -/** - * Sends a message packet. - * - * @param {String} message - * @param {Object} options - * @param {Function} callback - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.send = -Socket.prototype.write = function (data, options, callback) { - this.sendPacket('message', data, options, callback); - return this; -}; - -/** - * Sends a packet. - * - * @param {String} packet type - * @param {String} optional, data - * @param {Object} options - * @api private - */ - -Socket.prototype.sendPacket = function (type, data, options, callback) { - if ('function' === typeof options) { - callback = options; - options = null; - } - - options = options || {}; - options.compress = false !== options.compress; - - if ('closing' !== this.readyState && 'closed' !== this.readyState) { - debug('sending packet "%s" (%s)', type, data); - - var packet = { - type: type, - options: options - }; - if (data) packet.data = data; - - // exports packetCreate event - this.emit('packetCreate', packet); - - this.writeBuffer.push(packet); - - // add send callback to object, if defined - if (callback) this.packetsFn.push(callback); - - this.flush(); - } -}; - -/** - * Attempts to flush the packets buffer. - * - * @api private - */ - -Socket.prototype.flush = function () { - 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); - var wbuf = this.writeBuffer; - this.writeBuffer = []; - if (!this.transport.supportsFraming) { - this.sentCallbackFn.push(this.packetsFn); - } else { - this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn); - } - this.packetsFn = []; - this.transport.send(wbuf); - this.emit('drain'); - this.server.emit('drain', this); - } -}; - -/** - * Get available upgrades for this socket. - * - * @api private - */ - -Socket.prototype.getAvailableUpgrades = function () { - var availableUpgrades = []; - var allUpgrades = this.server.upgrades(this.transport.name); - for (var i = 0, l = allUpgrades.length; i < l; ++i) { - var upg = allUpgrades[i]; - if (this.server.transports.indexOf(upg) !== -1) { - availableUpgrades.push(upg); - } - } - return availableUpgrades; -}; - -/** - * Closes the socket and underlying transport. - * - * @param {Boolean} optional, discard - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.close = function (discard) { - if ('open' !== this.readyState) return; - - this.readyState = 'closing'; - - if (this.writeBuffer.length) { - this.once('drain', this.closeTransport.bind(this, discard)); - return; - } - - this.closeTransport(discard); -}; - -/** - * Closes the underlying transport. - * - * @param {Boolean} discard - * @api private - */ - -Socket.prototype.closeTransport = function (discard) { - if (discard) this.transport.discard(); - this.transport.close(this.onClose.bind(this, 'forced close')); -}; diff --git a/lib/socket.ts b/lib/socket.ts new file mode 100644 index 000000000..73b9b0f1f --- /dev/null +++ b/lib/socket.ts @@ -0,0 +1,556 @@ +import { EventEmitter } from "events"; +import debugModule from "debug"; +import { IncomingMessage } from "http"; +import { Transport } from "./transport"; +import { Server } from "./server"; +import { setTimeout, clearTimeout } from "timers"; +import { Packet, PacketType, RawData } from "engine.io-parser"; + +const debug = debugModule("engine:socket"); + +export class Socket extends EventEmitter { + public readonly protocol: number; + public readonly request: IncomingMessage; + public readonly remoteAddress: string; + + public _readyState: string; + public transport: Transport; + + private server: Server; + private upgrading: boolean; + private upgraded: boolean; + private writeBuffer: Packet[]; + private packetsFn: any[]; + private sentCallbackFn: any[]; + private cleanupFn: any[]; + private checkIntervalTimer; + private upgradeTimeoutTimer; + private pingTimeoutTimer; + private pingIntervalTimer; + + private readonly id: string; + + get readyState() { + return this._readyState; + } + + set readyState(state) { + debug("readyState updated from %s to %s", this._readyState, state); + this._readyState = state; + } + + /** + * Client class (abstract). + * + * @api private + */ + constructor(id, server, transport, req, protocol) { + super(); + this.id = id; + this.server = server; + this.upgrading = false; + this.upgraded = false; + this.readyState = "opening"; + this.writeBuffer = []; + this.packetsFn = []; + this.sentCallbackFn = []; + this.cleanupFn = []; + this.request = req; + this.protocol = protocol; + + // Cache IP since it might not be in the req later + if (req.websocket && req.websocket._socket) { + this.remoteAddress = req.websocket._socket.remoteAddress; + } else { + this.remoteAddress = req.connection.remoteAddress; + } + + this.checkIntervalTimer = null; + this.upgradeTimeoutTimer = null; + this.pingTimeoutTimer = null; + this.pingIntervalTimer = null; + + this.setTransport(transport); + this.onOpen(); + } + + /** + * Called upon transport considered open. + * + * @api 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( + this.server.opts.pingInterval + this.server.opts.pingTimeout + ); + } 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 + * @api 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); + + // Reset ping timeout on any packet, incoming data is a good sign of + // other side's liveness + this.resetPingTimeout( + this.server.opts.pingInterval + this.server.opts.pingTimeout + ); + + switch (packet.type) { + case "ping": + if (this.transport.protocol !== 3) { + this.onError("invalid heartbeat direction"); + return; + } + debug("got ping"); + this.sendPacket("pong"); + this.emit("heartbeat"); + break; + + case "pong": + if (this.transport.protocol === 3) { + this.onError("invalid heartbeat direction"); + return; + } + debug("got pong"); + 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} error object + * @api private + */ + private onError(err) { + debug("transport error"); + this.onClose("transport error", err); + } + + /** + * Pings client every `this.pingInterval` and expects response + * within `this.pingTimeout` or closes connection. + * + * @api 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.pingTimeout); + }, this.server.opts.pingInterval); + } + + /** + * Resets ping timeout. + * + * @api private + */ + private resetPingTimeout(timeout) { + clearTimeout(this.pingTimeoutTimer); + this.pingTimeoutTimer = setTimeout(() => { + if (this.readyState === "closed") return; + this.onClose("ping timeout"); + }, timeout); + } + + /** + * Attaches handlers for the given transport. + * + * @param {Transport} transport + * @api private + */ + private setTransport(transport) { + const onError = this.onError.bind(this); + const onPacket = this.onPacket.bind(this); + const flush = this.flush.bind(this); + const onClose = this.onClose.bind(this, "transport close"); + + this.transport = transport; + this.transport.once("error", onError); + this.transport.on("packet", onPacket); + this.transport.on("drain", flush); + this.transport.once("close", onClose); + // this function will manage packet events (also message callbacks) + this.setupSendCallback(); + + this.cleanupFn.push(function() { + transport.removeListener("error", onError); + transport.removeListener("packet", onPacket); + transport.removeListener("drain", flush); + transport.removeListener("close", onClose); + }); + } + + /** + * Upgrades socket to the given transport + * + * @param {Transport} transport + * @api private + */ + private maybeUpgrade(transport) { + debug( + 'might upgrade socket transport from "%s" to "%s"', + this.transport.name, + transport.name + ); + + this.upgrading = true; + + // set transport upgrade timer + this.upgradeTimeoutTimer = setTimeout(() => { + debug("client did not complete upgrade - closing transport"); + cleanup(); + if ("open" === transport.readyState) { + transport.close(); + } + }, this.server.opts.upgradeTimeout); + + 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(this.checkIntervalTimer); + this.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(this.checkIntervalTimer); + this.checkIntervalTimer = null; + + clearTimeout(this.upgradeTimeoutTimer); + this.upgradeTimeoutTimer = null; + + 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. + * + * @api 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); + + clearInterval(this.checkIntervalTimer); + this.checkIntervalTimer = null; + clearTimeout(this.upgradeTimeoutTimer); + // 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); + } + } + + /** + * Setup and manage send callback + * + * @api private + */ + private setupSendCallback() { + // the message was sent successfully, execute the callback + const onDrain = () => { + if (this.sentCallbackFn.length > 0) { + const seqFn = this.sentCallbackFn.splice(0, 1)[0]; + if ("function" === typeof seqFn) { + debug("executing send callback"); + seqFn(this.transport); + } else if (Array.isArray(seqFn)) { + debug("executing batch send callback"); + const l = seqFn.length; + let i = 0; + for (; i < l; i++) { + if ("function" === typeof seqFn[i]) { + seqFn[i](this.transport); + } + } + } + } + }; + + this.transport.on("drain", onDrain); + + this.cleanupFn.push(() => { + this.transport.removeListener("drain", onDrain); + }); + } + + /** + * Sends a message packet. + * + * @param {Object} data + * @param {Object} options + * @param {Function} callback + * @return {Socket} for chaining + * @api public + */ + public send(data, options, callback?) { + this.sendPacket("message", data, options, callback); + return this; + } + + public write(data, options, callback?) { + 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 + * + * @api private + */ + private sendPacket(type: PacketType, data?: RawData, options?, callback?) { + if ("function" === typeof options) { + callback = options; + options = null; + } + + options = options || {}; + options.compress = false !== options.compress; + + if ("closing" !== this.readyState && "closed" !== this.readyState) { + debug('sending packet "%s" (%s)', type, data); + + const packet: Packet = { + type, + options + }; + + if (data) packet.data = data; + + // exports packetCreate event + this.emit("packetCreate", packet); + + this.writeBuffer.push(packet); + + // add send callback to object, if defined + if (callback) this.packetsFn.push(callback); + + this.flush(); + } + } + + /** + * Attempts to flush the packets buffer. + * + * @api 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.transport.supportsFraming) { + this.sentCallbackFn.push(this.packetsFn); + } else { + this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn); + } + this.packetsFn = []; + this.transport.send(wbuf); + this.emit("drain"); + this.server.emit("drain", this); + } + } + + /** + * Get available upgrades for this socket. + * + * @api private + */ + private getAvailableUpgrades() { + const availableUpgrades = []; + const allUpgrades = this.server.upgrades(this.transport.name); + let i = 0; + const l = allUpgrades.length; + for (; i < l; ++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 + * @api public + */ + public close(discard?: boolean) { + if ("open" !== this.readyState) return; + + this.readyState = "closing"; + + if (this.writeBuffer.length) { + this.once("drain", this.closeTransport.bind(this, discard)); + return; + } + + this.closeTransport(discard); + } + + /** + * Closes the underlying transport. + * + * @param {Boolean} discard + * @api private + */ + private closeTransport(discard) { + if (discard) this.transport.discard(); + this.transport.close(this.onClose.bind(this, "forced close")); + } +} diff --git a/lib/transport.js b/lib/transport.js deleted file mode 100644 index 933dad5bb..000000000 --- a/lib/transport.js +++ /dev/null @@ -1,128 +0,0 @@ - -/** - * Module dependencies. - */ - -var EventEmitter = require('events').EventEmitter; -var parser = require('engine.io-parser'); -var util = require('util'); -var debug = require('debug')('engine:transport'); - -/** - * Expose the constructor. - */ - -module.exports = Transport; - -/** - * Noop function. - * - * @api private - */ - -function noop () {} - -/** - * Transport constructor. - * - * @param {http.IncomingMessage} request - * @api public - */ - -function Transport (req) { - this.readyState = 'open'; - this.discarded = false; -} - -/** - * Inherits from EventEmitter. - */ - -util.inherits(Transport, EventEmitter); - -/** - * Flags the transport as discarded. - * - * @api private - */ - -Transport.prototype.discard = function () { - this.discarded = true; -}; - -/** - * Called with an incoming HTTP request. - * - * @param {http.IncomingMessage} request - * @api private - */ - -Transport.prototype.onRequest = function (req) { - debug('setting request'); - this.req = req; -}; - -/** - * Closes the transport. - * - * @api private - */ - -Transport.prototype.close = function (fn) { - if ('closed' === this.readyState || 'closing' === this.readyState) return; - - this.readyState = 'closing'; - this.doClose(fn || noop); -}; - -/** - * Called with a transport error. - * - * @param {String} message error - * @param {Object} error description - * @api private - */ - -Transport.prototype.onError = function (msg, desc) { - if (this.listeners('error').length) { - var err = new Error(msg); - err.type = 'TransportError'; - 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 - * @api private - */ - -Transport.prototype.onPacket = function (packet) { - this.emit('packet', packet); -}; - -/** - * Called with the encoded packet data. - * - * @param {String} data - * @api private - */ - -Transport.prototype.onData = function (data) { - this.onPacket(parser.decodePacket(data)); -}; - -/** - * Called upon transport close. - * - * @api private - */ - -Transport.prototype.onClose = function () { - this.readyState = 'closed'; - this.emit('close'); -}; diff --git a/lib/transport.ts b/lib/transport.ts new file mode 100644 index 000000000..70bc9cfa3 --- /dev/null +++ b/lib/transport.ts @@ -0,0 +1,143 @@ +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 { IncomingMessage } from "http"; +import { Packet } from "engine.io-parser"; + +const debug = debugModule("engine:transport"); + +/** + * Noop function. + * + * @api private + */ + +function noop() {} + +export abstract class Transport extends EventEmitter { + public sid: string; + public writable: boolean; + public protocol: number; + + protected _readyState: string; + protected discarded: boolean; + protected parser: any; + protected req: IncomingMessage & { cleanup: Function }; + protected supportsBinary: boolean; + + get readyState() { + return this._readyState; + } + + set readyState(state) { + debug( + "readyState updated from %s to %s (%s)", + this._readyState, + state, + this.name + ); + this._readyState = state; + } + + /** + * Transport constructor. + * + * @param {http.IncomingMessage} request + * @api public + */ + constructor(req) { + super(); + this.readyState = "open"; + this.discarded = false; + this.protocol = req._query.EIO === "4" ? 4 : 3; // 3rd revision by default + this.parser = this.protocol === 4 ? parser_v4 : parser_v3; + } + + /** + * Flags the transport as discarded. + * + * @api private + */ + discard() { + this.discarded = true; + } + + /** + * Called with an incoming HTTP request. + * + * @param {http.IncomingMessage} request + * @api protected + */ + protected onRequest(req) { + debug("setting request"); + this.req = req; + } + + /** + * Closes the transport. + * + * @api private + */ + close(fn?) { + if ("closed" === this.readyState || "closing" === this.readyState) return; + + this.readyState = "closing"; + this.doClose(fn || noop); + } + + /** + * Called with a transport error. + * + * @param {String} message error + * @param {Object} error description + * @api 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 + * @api protected + */ + protected onPacket(packet: Packet) { + this.emit("packet", packet); + } + + /** + * Called with the encoded packet data. + * + * @param {String} data + * @api protected + */ + protected onData(data) { + this.onPacket(this.parser.decodePacket(data)); + } + + /** + * Called upon transport close. + * + * @api protected + */ + protected onClose() { + this.readyState = "closed"; + this.emit("close"); + } + + abstract get supportsFraming(); + abstract get name(); + abstract send(packets); + abstract doClose(fn?); +} diff --git a/lib/transports-uws/index.ts b/lib/transports-uws/index.ts new file mode 100644 index 000000000..d015645e7 --- /dev/null +++ b/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/lib/transports-uws/polling.ts b/lib/transports-uws/polling.ts new file mode 100644 index 000000000..67581abd1 --- /dev/null +++ b/lib/transports-uws/polling.ts @@ -0,0 +1,425 @@ +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 res: HttpResponse; + private dataReq: HttpRequest; + private dataRes: HttpResponse; + private shouldClose: Function; + + private readonly closeTimeout: number; + + /** + * HTTP polling constructor. + * + * @api public. + */ + constructor(req) { + super(req); + + this.closeTimeout = 30 * 1000; + } + + /** + * Transport name + * + * @api public + */ + get name() { + return "polling"; + } + + get supportsFraming() { + return false; + } + + /** + * Overrides onRequest. + * + * @param req + * + * @api private + */ + onRequest(req) { + const res = req.res; + + 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. + * + * @api 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("drain"); + + // 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. + * + * @api 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.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. + * + * @api private + */ + private onDataRequestCleanup() { + this.dataReq = this.dataRes = null; + } + + /** + * Processes the incoming data payload. + * + * @param {String} encoded payload + * @api 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. + * + * @api private + */ + onClose() { + if (this.writable) { + // close pending poll request + this.send([{ type: "noop" }]); + } + super.onClose(); + } + + /** + * Writes a packet payload. + * + * @param {Object} packet + * @api 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 + * @api private + */ + write(data, options) { + debug('writing "%s"', data); + this.doWrite(data, options, () => { + this.req.cleanup(); + }); + } + + /** + * Performs the write. + * + * @api 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); + 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. + * + * @api 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. + * + * @api 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 + * @api 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"; + } + + this.emit("headers", headers, req); + return headers; + } +} diff --git a/lib/transports-uws/websocket.ts b/lib/transports-uws/websocket.ts new file mode 100644 index 000000000..3add4baab --- /dev/null +++ b/lib/transports-uws/websocket.ts @@ -0,0 +1,100 @@ +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 + * @api public + */ + constructor(req) { + super(req); + this.writable = false; + this.perMessageDeflate = null; + } + + /** + * Transport name + * + * @api public + */ + get name() { + return "websocket"; + } + + /** + * Advertise upgrade support. + * + * @api public + */ + get handlesUpgrades() { + return true; + } + + /** + * Advertise framing support. + * + * @api public + */ + get supportsFraming() { + return true; + } + + /** + * Writes a packet payload. + * + * @param {Array} packets + * @api private + */ + send(packets) { + const packet = packets.shift(); + if (typeof packet === "undefined") { + this.writable = true; + this.emit("drain"); + return; + } + + // always creates a new object since ws modifies it + const opts: { compress?: boolean } = {}; + if (packet.options) { + opts.compress = packet.options.compress; + } + + const send = data => { + const isBinary = typeof data !== "string"; + const compress = + this.perMessageDeflate && + Buffer.byteLength(data) > this.perMessageDeflate.threshold; + + debug('writing "%s"', data); + this.writable = false; + + this.socket.send(data, isBinary, compress); + this.send(packets); + }; + + if (packet.options && typeof packet.options.wsPreEncoded === "string") { + send(packet.options.wsPreEncoded); + } else { + this.parser.encodePacket(packet, this.supportsBinary, send); + } + } + + /** + * Closes the transport. + * + * @api private + */ + doClose(fn) { + debug("closing"); + fn && fn(); + // call fn first since socket.close() immediately emits a "close" event + this.socket.close(); + } +} diff --git a/lib/transports/index.js b/lib/transports/index.js deleted file mode 100644 index fcff3223a..000000000 --- a/lib/transports/index.js +++ /dev/null @@ -1,36 +0,0 @@ - -/** - * Module dependencies. - */ - -var XHR = require('./polling-xhr'); -var JSONP = require('./polling-jsonp'); - -/** - * Export transports. - */ - -module.exports = exports = { - polling: polling, - websocket: require('./websocket') -}; - -/** - * Export upgrades map. - */ - -exports.polling.upgradesTo = ['websocket']; - -/** - * Polling polymorphic constructor. - * - * @api private - */ - -function polling (req) { - if ('string' === typeof req._query.j) { - return new JSONP(req); - } else { - return new XHR(req); - } -} diff --git a/lib/transports/index.ts b/lib/transports/index.ts new file mode 100644 index 000000000..55065bd66 --- /dev/null +++ b/lib/transports/index.ts @@ -0,0 +1,24 @@ +import { Polling as XHR } from "./polling"; +import { JSONP } from "./polling-jsonp"; +import { WebSocket } from "./websocket"; + +export default { + polling: polling, + websocket: WebSocket +}; + +/** + * Polling polymorphic constructor. + * + * @api private + */ + +function polling(req) { + if ("string" === typeof req._query.j) { + return new JSONP(req); + } else { + return new XHR(req); + } +} + +polling.upgradesTo = ["websocket"]; diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js deleted file mode 100644 index 62e66e779..000000000 --- a/lib/transports/polling-jsonp.js +++ /dev/null @@ -1,75 +0,0 @@ - -/** - * Module dependencies. - */ - -var Polling = require('./polling'); -var qs = require('querystring'); -var rDoubleSlashes = /\\\\n/g; -var rSlashes = /(\\)?\\n/g; -var util = require('util'); - -/** - * Module exports. - */ - -module.exports = JSONP; - -/** - * JSON-P polling transport. - * - * @api public - */ - -function JSONP (req) { - Polling.call(this, req); - - this.head = '___eio[' + (req._query.j || '').replace(/[^0-9]/g, '') + ']('; - this.foot = ');'; -} - -/** - * Inherits from Polling. - */ - -util.inherits(JSONP, Polling); - -/** - * Handles incoming data. - * Due to a bug in \n handling by browsers, we expect a escaped string. - * - * @api private - */ - -JSONP.prototype.onData = function (data) { - // we leverage the qs module so that we get built-in DoS protection - // and the fast alternative to decodeURIComponent - data = qs.parse(data).d; - 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'; - }); - Polling.prototype.onData.call(this, data.replace(rDoubleSlashes, '\\n')); - } -}; - -/** - * Performs the write. - * - * @api private - */ - -JSONP.prototype.doWrite = function (data, options, callback) { - // we must output valid javascript, not valid json - // see: http://timelessrepo.com/json-isnt-a-javascript-subset - var js = JSON.stringify(data) - .replace(/\u2028/g, '\\u2028') - .replace(/\u2029/g, '\\u2029'); - - // prepare response - data = this.head + js + this.foot; - - Polling.prototype.doWrite.call(this, data, options, callback); -}; diff --git a/lib/transports/polling-jsonp.ts b/lib/transports/polling-jsonp.ts new file mode 100644 index 000000000..b40f935e9 --- /dev/null +++ b/lib/transports/polling-jsonp.ts @@ -0,0 +1,60 @@ +import { Polling } from "./polling"; +import * as qs from "querystring"; + +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. + * + * @api public + */ + constructor(req) { + super(req); + + this.head = "___eio[" + (req._query.j || "").replace(/[^0-9]/g, "") + "]("; + this.foot = ");"; + } + + /** + * Handles incoming data. + * Due to a bug in \n handling by browsers, we expect a escaped string. + * + * @api private + */ + onData(data) { + // we leverage the qs module so that we get built-in DoS protection + // and the fast alternative to decodeURIComponent + data = qs.parse(data).d; + 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")); + } + } + + /** + * Performs the write. + * + * @api private + */ + 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/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js deleted file mode 100644 index 3562524e1..000000000 --- a/lib/transports/polling-xhr.js +++ /dev/null @@ -1,69 +0,0 @@ - -/** - * Module dependencies. - */ - -var Polling = require('./polling'); -var util = require('util'); - -/** - * Module exports. - */ - -module.exports = XHR; - -/** - * Ajax polling transport. - * - * @api public - */ - -function XHR (req) { - Polling.call(this, req); -} - -/** - * Inherits from Polling. - */ - -util.inherits(XHR, Polling); - -/** - * Overrides `onRequest` to handle `OPTIONS`.. - * - * @param {http.IncomingMessage} - * @api private - */ - -XHR.prototype.onRequest = function (req) { - if ('OPTIONS' === req.method) { - var res = req.res; - var headers = this.headers(req); - headers['Access-Control-Allow-Headers'] = 'Content-Type'; - res.writeHead(200, headers); - res.end(); - } else { - Polling.prototype.onRequest.call(this, req); - } -}; - -/** - * Returns headers for a response. - * - * @param {http.IncomingMessage} request - * @param {Object} extra headers - * @api private - */ - -XHR.prototype.headers = function (req, headers) { - headers = headers || {}; - - if (req.headers.origin) { - headers['Access-Control-Allow-Credentials'] = 'true'; - headers['Access-Control-Allow-Origin'] = req.headers.origin; - } else { - headers['Access-Control-Allow-Origin'] = '*'; - } - - return Polling.prototype.headers.call(this, req, headers); -}; diff --git a/lib/transports/polling.js b/lib/transports/polling.js deleted file mode 100644 index 7c29c29ea..000000000 --- a/lib/transports/polling.js +++ /dev/null @@ -1,407 +0,0 @@ - -/** - * Module requirements. - */ - -var Transport = require('../transport'); -var parser = require('engine.io-parser'); -var zlib = require('zlib'); -var accepts = require('accepts'); -var util = require('util'); -var debug = require('debug')('engine:polling'); - -var compressionMethods = { - gzip: zlib.createGzip, - deflate: zlib.createDeflate -}; - -/** - * Exports the constructor. - */ - -module.exports = Polling; - -/** - * HTTP polling constructor. - * - * @api public. - */ - -function Polling (req) { - Transport.call(this, req); - - this.closeTimeout = 30 * 1000; - this.maxHttpBufferSize = null; - this.httpCompression = null; -} - -/** - * Inherits from Transport. - * - * @api public. - */ - -util.inherits(Polling, Transport); - -/** - * Transport name - * - * @api public - */ - -Polling.prototype.name = 'polling'; - -/** - * Overrides onRequest. - * - * @param {http.IncomingMessage} - * @api private - */ - -Polling.prototype.onRequest = function (req) { - var res = req.res; - - 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. - * - * @api private - */ - -Polling.prototype.onPollRequest = function (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.writeHead(500); - res.end(); - return; - } - - debug('setting request'); - - this.req = req; - this.res = res; - - var self = this; - - function onClose () { - self.onError('poll connection closed prematurely'); - } - - function cleanup () { - req.removeListener('close', onClose); - self.req = self.res = null; - } - - req.cleanup = cleanup; - req.on('close', onClose); - - this.writable = true; - this.emit('drain'); - - // 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. - * - * @api private - */ - -Polling.prototype.onDataRequest = function (req, res) { - if (this.dataReq) { - // assert: this.dataRes, '.dataReq and .dataRes should be (un)set together' - this.onError('data request overlap from client'); - res.writeHead(500); - res.end(); - return; - } - - var isBinary = 'application/octet-stream' === req.headers['content-type']; - - this.dataReq = req; - this.dataRes = res; - - var chunks = isBinary ? Buffer.concat([]) : ''; - var self = this; - - function cleanup () { - req.removeListener('data', onData); - req.removeListener('end', onEnd); - req.removeListener('close', onClose); - self.dataReq = self.dataRes = chunks = null; - } - - function onClose () { - cleanup(); - self.onError('data request connection closed prematurely'); - } - - function onData (data) { - var contentLength; - if (isBinary) { - chunks = Buffer.concat([chunks, data]); - contentLength = chunks.length; - } else { - chunks += data; - contentLength = Buffer.byteLength(chunks); - } - - if (contentLength > self.maxHttpBufferSize) { - chunks = isBinary ? Buffer.concat([]) : ''; - req.connection.destroy(); - } - } - - function onEnd () { - self.onData(chunks); - - var 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, self.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 {String} encoded payload - * @api private - */ - -Polling.prototype.onData = function (data) { - debug('received "%s"', data); - var self = this; - var callback = function (packet) { - if ('close' === packet.type) { - debug('got xhr close packet'); - self.onClose(); - return false; - } - - self.onPacket(packet); - }; - - parser.decodePayload(data, callback); -}; - -/** - * Overrides onClose. - * - * @api private - */ - -Polling.prototype.onClose = function () { - if (this.writable) { - // close pending poll request - this.send([{ type: 'noop' }]); - } - Transport.prototype.onClose.call(this); -}; - -/** - * Writes a packet payload. - * - * @param {Object} packet - * @api private - */ - -Polling.prototype.send = function (packets) { - this.writable = false; - - if (this.shouldClose) { - debug('appending close packet to payload'); - packets.push({ type: 'close' }); - this.shouldClose(); - this.shouldClose = null; - } - - var self = this; - parser.encodePayload(packets, this.supportsBinary, function (data) { - var compress = packets.some(function (packet) { - return packet.options && packet.options.compress; - }); - self.write(data, { compress: compress }); - }); -}; - -/** - * Writes data as response to poll request. - * - * @param {String} data - * @param {Object} options - * @api private - */ - -Polling.prototype.write = function (data, options) { - debug('writing "%s"', data); - var self = this; - this.doWrite(data, options, function () { - self.req.cleanup(); - }); -}; - -/** - * Performs the write. - * - * @api private - */ - -Polling.prototype.doWrite = function (data, options, callback) { - var self = this; - - // explicit UTF-8 is required for pages not served under utf - var isString = typeof data === 'string'; - var contentType = isString - ? 'text/plain; charset=UTF-8' - : 'application/octet-stream'; - - var headers = { - 'Content-Type': contentType - }; - - if (!this.httpCompression || !options.compress) { - respond(data); - return; - } - - var len = isString ? Buffer.byteLength(data) : data.length; - if (len < this.httpCompression.threshold) { - respond(data); - return; - } - - var encoding = accepts(this.req).encodings(['gzip', 'deflate']); - if (!encoding) { - respond(data); - return; - } - - this.compress(data, encoding, function (err, data) { - if (err) { - self.res.writeHead(500); - self.res.end(); - callback(err); - return; - } - - headers['Content-Encoding'] = encoding; - respond(data); - }); - - function respond (data) { - headers['Content-Length'] = 'string' === typeof data ? Buffer.byteLength(data) : data.length; - self.res.writeHead(200, self.headers(self.req, headers)); - self.res.end(data); - callback(); - } -}; - -/** - * Compresses data. - * - * @api private - */ - -Polling.prototype.compress = function (data, encoding, callback) { - debug('compressing'); - - var buffers = []; - var 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. - * - * @api private - */ - -Polling.prototype.doClose = function (fn) { - debug('closing'); - - var self = this; - var closeTimeoutTimer; - - if (this.dataReq) { - debug('aborting ongoing data request'); - this.dataReq.destroy(); - } - - 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); - } - - function onClose () { - clearTimeout(closeTimeoutTimer); - fn(); - self.onClose(); - } -}; - -/** - * Returns headers for a response. - * - * @param {http.IncomingMessage} request - * @param {Object} extra headers - * @api private - */ - -Polling.prototype.headers = function (req, headers) { - headers = headers || {}; - - // prevent XSS warnings on IE - // https://github.com/LearnBoost/socket.io/pull/1333 - var ua = req.headers['user-agent']; - if (ua && (~ua.indexOf(';MSIE') || ~ua.indexOf('Trident/'))) { - headers['X-XSS-Protection'] = '0'; - } - - this.emit('headers', headers); - return headers; -}; diff --git a/lib/transports/polling.ts b/lib/transports/polling.ts new file mode 100644 index 000000000..fdb4d2644 --- /dev/null +++ b/lib/transports/polling.ts @@ -0,0 +1,398 @@ +import { Transport } from "../transport"; +import { createGzip, createDeflate } from "zlib"; +import * as accepts from "accepts"; +import debugModule from "debug"; +import { IncomingMessage, ServerResponse } from "http"; + +const debug = debugModule("engine:polling"); + +const compressionMethods = { + gzip: createGzip, + deflate: createDeflate +}; + +export class Polling extends Transport { + public maxHttpBufferSize: number; + public httpCompression: any; + + private res: ServerResponse; + private dataReq: IncomingMessage; + private dataRes: ServerResponse; + private shouldClose: Function; + + private readonly closeTimeout: number; + + /** + * HTTP polling constructor. + * + * @api public. + */ + constructor(req) { + super(req); + + this.closeTimeout = 30 * 1000; + } + + /** + * Transport name + * + * @api public + */ + get name() { + return "polling"; + } + + get supportsFraming() { + return false; + } + + /** + * Overrides onRequest. + * + * @param {http.IncomingMessage} + * @api private + */ + onRequest(req: IncomingMessage & { res: ServerResponse }) { + const res = req.res; + + 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. + * + * @api 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.writeHead(500); + 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("drain"); + + // 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. + * + * @api 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(500); + 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 {String} encoded payload + * @api 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. + * + * @api private + */ + onClose() { + if (this.writable) { + // close pending poll request + this.send([{ type: "noop" }]); + } + super.onClose(); + } + + /** + * Writes a packet payload. + * + * @param {Object} packet + * @api 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 + * @api private + */ + write(data, options) { + debug('writing "%s"', data); + this.doWrite(data, options, () => { + this.req.cleanup(); + }); + } + + /** + * Performs the write. + * + * @api 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 => { + 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. + * + * @api 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. + * + * @api private + */ + doClose(fn) { + 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} request + * @param {Object} extra headers + * @api 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"; + } + + this.emit("headers", headers, req); + return headers; + } +} diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js deleted file mode 100644 index 7d5511b1d..000000000 --- a/lib/transports/websocket.js +++ /dev/null @@ -1,134 +0,0 @@ - -/** - * Module dependencies. - */ - -var Transport = require('../transport'); -var parser = require('engine.io-parser'); -var util = require('util'); -var debug = require('debug')('engine:ws'); - -/** - * Export the constructor. - */ - -module.exports = WebSocket; - -/** - * WebSocket transport - * - * @param {http.IncomingMessage} - * @api public - */ - -function WebSocket (req) { - Transport.call(this, req); - var self = this; - this.socket = req.websocket; - this.socket.on('message', this.onData.bind(this)); - this.socket.once('close', this.onClose.bind(this)); - this.socket.on('error', this.onError.bind(this)); - this.socket.on('headers', onHeaders); - this.writable = true; - this.perMessageDeflate = null; - - function onHeaders (headers) { - self.emit('headers', headers); - } -} - -/** - * Inherits from Transport. - */ - -util.inherits(WebSocket, Transport); - -/** - * Transport name - * - * @api public - */ - -WebSocket.prototype.name = 'websocket'; - -/** - * Advertise upgrade support. - * - * @api public - */ - -WebSocket.prototype.handlesUpgrades = true; - -/** - * Advertise framing support. - * - * @api public - */ - -WebSocket.prototype.supportsFraming = true; - -/** - * Processes the incoming data. - * - * @param {String} encoded packet - * @api private - */ - -WebSocket.prototype.onData = function (data) { - debug('received "%s"', data); - Transport.prototype.onData.call(this, data); -}; - -/** - * Writes a packet payload. - * - * @param {Array} packets - * @api private - */ - -WebSocket.prototype.send = function (packets) { - var self = this; - - for (var i = 0; i < packets.length; i++) { - var packet = packets[i]; - parser.encodePacket(packet, self.supportsBinary, send); - } - - function send (data) { - debug('writing "%s"', data); - - // always creates a new object since ws modifies it - var opts = {}; - if (packet.options) { - opts.compress = packet.options.compress; - } - - if (self.perMessageDeflate) { - var len = 'string' === typeof data ? Buffer.byteLength(data) : data.length; - if (len < self.perMessageDeflate.threshold) { - opts.compress = false; - } - } - - self.writable = false; - self.socket.send(data, opts, onEnd); - } - - function onEnd (err) { - if (err) return self.onError('write error', err.stack); - self.writable = true; - self.emit('drain'); - } -}; - -/** - * Closes the transport. - * - * @api private - */ - -WebSocket.prototype.doClose = function (fn) { - debug('closing'); - this.socket.close(); - fn && fn(); -}; diff --git a/lib/transports/websocket.ts b/lib/transports/websocket.ts new file mode 100644 index 000000000..4e868707e --- /dev/null +++ b/lib/transports/websocket.ts @@ -0,0 +1,111 @@ +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 {http.IncomingMessage} + * @api public + */ + constructor(req) { + 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 + * + * @api public + */ + get name() { + return "websocket"; + } + + /** + * Advertise upgrade support. + * + * @api public + */ + get handlesUpgrades() { + return true; + } + + /** + * Advertise framing support. + * + * @api public + */ + get supportsFraming() { + return true; + } + + /** + * Writes a packet payload. + * + * @param {Array} packets + * @api private + */ + send(packets) { + const packet = packets.shift(); + if (typeof packet === "undefined") { + this.writable = true; + this.emit("drain"); + return; + } + + // always creates a new object since ws modifies it + const opts: { compress?: boolean } = {}; + if (packet.options) { + opts.compress = packet.options.compress; + } + + const send = data => { + if (this.perMessageDeflate) { + const len = + "string" === typeof data ? Buffer.byteLength(data) : data.length; + if (len < this.perMessageDeflate.threshold) { + opts.compress = false; + } + } + debug('writing "%s"', data); + this.writable = false; + + this.socket.send(data, opts, err => { + if (err) return this.onError("write error", err.stack); + this.send(packets); + }); + }; + + if (packet.options && typeof packet.options.wsPreEncoded === "string") { + send(packet.options.wsPreEncoded); + } else { + this.parser.encodePacket(packet, this.supportsBinary, send); + } + } + + /** + * Closes the transport. + * + * @api private + */ + doClose(fn) { + debug("closing"); + this.socket.close(); + fn && fn(); + } +} diff --git a/lib/userver.ts b/lib/userver.ts new file mode 100644 index 000000000..66fa40414 --- /dev/null +++ b/lib/userver.ts @@ -0,0 +1,285 @@ +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. + * + * @api private + */ + private prepare(req, res: HttpResponse) { + req.method = req.getMethod().toUpperCase(); + + 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 = (options.path || "/engine.io").replace(/\/$/, "") + "/"; + (app as TemplatedApp) + .any(path, this.handleRequest.bind(this)) + // + .ws(path, { + compression: options.compression, + idleTimeout: options.idleTimeout, + maxBackpressure: options.maxBackpressure, + maxPayloadLength: this.opts.maxHttpBufferSize, + upgrade: this.handleUpgrade.bind(this), + open: ws => { + ws.transport.socket = ws; + ws.transport.writable = true; + ws.transport.emit("drain"); + }, + message: (ws, message, isBinary) => { + ws.transport.onData( + isBinary ? message : Buffer.from(message).toString() + ); + }, + close: (ws, code, message) => { + ws.transport.onClose(code, message); + } + }); + } + + 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"); + 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); + } + }; + + if (this.corsMiddleware) { + // needed to buffer headers until the status is computed + req.res = new ResponseWrapper(res); + + this.corsMiddleware.call(null, req, req.res, () => { + this.verify(req, false, callback); + }); + } else { + this.verify(req, false, callback); + } + } + + private handleUpgrade( + res: HttpResponse, + req: HttpRequest & { _query: any }, + context + ) { + debug("on upgrade"); + + this.prepare(req, res); + + // @ts-ignore + req.res = res; + + this.verify(req, true, async (errorCode, errorContext) => { + if (errorCode) { + 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; + } + } + + res.upgrade( + { + transport + }, + req.getHeader("sec-websocket-key"), + req.getHeader("sec-websocket-protocol"), + req.getHeader("sec-websocket-extensions"), + context + ); + }); + } + + 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 = []; + + constructor(readonly res: HttpResponse) {} + + public set statusCode(status: number) { + this.writeStatus(status === 200 ? "200 OK" : "204 No Content"); + } + + public setHeader(key, value) { + this.writeHeader(key, value); + } + + // needed by vary: https://github.com/jshttp/vary/blob/5d725d059b3871025cf753e9dfa08924d0bcfa8f/index.js#L134 + public getHeader() {} + + public writeStatus(status: string) { + this.res.writeStatus(status); + this.statusWritten = true; + this.writeBufferedHeaders(); + } + + public writeHeader(key: string, value: string) { + 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.statusWritten) { + // status will be inferred as "200 OK" + this.writeBufferedHeaders(); + } + this.res.end(data); + } + + public onData(fn) { + this.res.onData(fn); + } + + public onAborted(fn) { + this.res.onAborted(fn); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..92ad6281d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3345 @@ +{ + "name": "engine.io", + "version": "6.2.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "engine.io", + "version": "6.2.0", + "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.0.3", + "ws": "~8.2.3" + }, + "devDependencies": { + "babel-eslint": "^8.0.2", + "eiows": "^4.1.2", + "engine.io-client": "6.2.0", + "engine.io-client-v3": "npm:engine.io-client@3.5.2", + "expect.js": "^0.3.1", + "mocha": "^9.1.3", + "prettier": "^1.19.1", + "rimraf": "^3.0.2", + "superagent": "^3.8.1", + "typescript": "^4.4.3", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.15.0" + }, + "engines": { + "node": ">=10.0.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/@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", + "engines": { + "node": ">= 0.6.0" + } + }, + "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/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/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.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.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/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/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.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "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/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/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/engine.io-client": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.0.tgz", + "integrity": "sha512-gz4quwdj6i17TihLOUv1mZ7/AcsrS0/Oq1ggmqod9/ULDraBIptYoZsyISLD9+chnvN6QmSZD7zuBGuiNk1hRw==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3", + "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-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "dependencies": { + "@socket.io/base64-arraybuffer": "~1.0.2" + }, + "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/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/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/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "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.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/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/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-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/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/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/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/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/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/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/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/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/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/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/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "dev": true, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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/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/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/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/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.15.0", + "resolved": "git+https://git@github.com/uNetworking/uWebSockets.js.git#77bc1fd5577ae42b56675912eb8481a31f3fefd2", + "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.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "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/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" + } + }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, + "@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 + }, + "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 + }, + "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.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.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 + }, + "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" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "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 + }, + "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 + }, + "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 + }, + "engine.io-client": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.0.tgz", + "integrity": "sha512-gz4quwdj6i17TihLOUv1mZ7/AcsrS0/Oq1ggmqod9/ULDraBIptYoZsyISLD9+chnvN6QmSZD7zuBGuiNk1hRw==", + "dev": true, + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "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.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, + "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 + }, + "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 + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "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.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 + }, + "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 + }, + "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-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 + }, + "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 + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "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=" + }, + "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 + }, + "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 + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "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 + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "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" + } + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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 + }, + "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+https://git@github.com/uNetworking/uWebSockets.js.git#77bc1fd5577ae42b56675912eb8481a31f3fefd2", + "dev": true, + "from": "uWebSockets.js@github:uNetworking/uWebSockets.js#v20.15.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.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "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 + }, + "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/package.json b/package.json index efe90d83f..a4eb25412 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,14 @@ { "name": "engine.io", - "version": "3.2.1", + "version": "6.2.1", "description": "The realtime engine behind Socket.IO. Provides the foundation of a bidirectional connection between client and server", - "main": "lib/engine.io", + "type": "commonjs", + "main": "./build/engine.io.js", + "types": "./build/engine.io.d.ts", + "exports": { + "import": "./wrapper.mjs", + "require": "./build/engine.io.js" + }, "author": "Guillermo Rauch ", "homepage": "https://github.com/socketio/engine.io", "contributors": [ @@ -25,38 +31,50 @@ ], "license": "MIT", "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", - "base64id": "1.0.0", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1", - "cookie": "0.3.1" + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" }, "devDependencies": { "babel-eslint": "^8.0.2", - "babel-preset-es2015": "^6.24.0", - "engine.io-client": "3.2.1", - "eslint": "^4.5.0", - "eslint-config-standard": "^10.2.1", - "eslint-plugin-import": "^2.7.0", - "eslint-plugin-node": "^5.1.1", - "eslint-plugin-promise": "^3.5.0", - "eslint-plugin-standard": "^3.0.1", + "eiows": "^4.1.2", + "engine.io-client": "6.2.0", + "engine.io-client-v3": "npm:engine.io-client@3.5.2", "expect.js": "^0.3.1", - "mocha": "^4.0.1", - "s": "0.1.1", + "mocha": "^9.1.3", + "prettier": "^1.19.1", + "rimraf": "^3.0.2", "superagent": "^3.8.1", - "uws": "~9.14.0" + "typescript": "^4.4.3", + "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.15.0" }, "scripts": { - "lint": "eslint lib/ test/ *.js", - "test": "npm run lint && mocha && EIO_WS_ENGINE=uws mocha" + "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 \"lib/**/*.ts\" \"test/**/*.js\"", + "format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.js\"", + "prepack": "npm run compile" }, "repository": { "type": "git", "url": "git@github.com:socketio/engine.io.git" }, "files": [ - "lib/" - ] + "build/", + "wrapper.mjs" + ], + "engines": { + "node": ">=10.0.0" + } } diff --git a/test/common.js b/test/common.js index 1f7244445..bb3cebdbb 100644 --- a/test/common.js +++ b/test/common.js @@ -1,29 +1,57 @@ - -/** - * Module dependencies. - */ - -var eio = require('..'); +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 = function (opts, fn) { - if ('function' === typeof opts) { +exports.listen = (opts, fn) => { + if ("function" === typeof opts) { fn = opts; opts = {}; } - var e = eio.listen(0, opts, function () { + 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; }; -/** - * Sprintf util. - */ - -require('s').extend(); +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/test/engine.io.js b/test/engine.io.js index 9387094c9..a0abe7625 100644 --- a/test/engine.io.js +++ b/test/engine.io.js @@ -1,41 +1,44 @@ - -/** - * Test dependencies. - */ - -var net = require('net'); -var eio = require('..'); -var listen = require('./common').listen; -var expect = require('expect.js'); -var request = require('superagent'); -var http = require('http'); +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', function () { - it('should expose protocol number', function () { - expect(eio.protocol).to.be.a('number'); +describe("engine", () => { + it("should expose protocol number", () => { + expect(protocol).to.be.a("number"); }); - it('should be the same version as client', function () { - var version = require('../package').version; - expect(version).to.be(require('engine.io-client/package').version); + 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()', function () { - it('should create a Server when require called with no arguments', function () { - var engine = eio(); - expect(engine).to.be.an(eio.Server); + 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', function () { - it('should open a http server that returns 501', function (done) { - listen(function (port) { - request.get('http://localhost:%d/'.s(port), function (err, res) { + 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(); @@ -44,192 +47,206 @@ describe('engine', function () { }); }); - describe('attach()', function () { - it('should work from require()', function () { - var server = http.createServer(); - var engine = eio(server); + describe("attach()", () => { + it("should work from require()", () => { + const server = http.createServer(); + const engine = new Server(server); - expect(engine).to.be.an(eio.Server); + expect(engine).to.be.an(Server); }); - it('should return an engine.Server', function () { - var server = http.createServer(); - var engine = eio.attach(server); + it("should return an engine.Server", () => { + const server = http.createServer(); + const engine = attach(server); - expect(engine).to.be.an(eio.Server); + expect(engine).to.be.an(Server); }); - it('should attach engine to an http server', function (done) { - var server = http.createServer(); - eio.attach(server); + it("should attach engine to an http server", done => { + const server = http.createServer(); + attach(server); - server.listen(function () { - var uri = 'http://localhost:%d/engine.io/default/'.s(server.address().port); - request.get(uri, function (err, res) { + 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); + expect(res.body.message).to.be("Transport unknown"); + server.once("close", done); server.close(); }); }); }); - it('should destroy upgrades not handled by engine', function (done) { - var server = http.createServer(); - eio.attach(server, { destroyUpgradeTimeout: 50 }); - - server.listen(function () { - var client = net.createConnection(server.address().port); - client.setEncoding('ascii'); - client.write([ - 'GET / HTTP/1.1', - 'Connection: Upgrade', - 'Upgrade: IRC/6.9', - '', '' - ].join('\r\n')); - - var check = setTimeout(function () { - done(new Error('Client should have ended')); + 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', function () { + client.on("end", () => { clearTimeout(check); done(); }); }); }); - it('should not destroy unhandled upgrades with destroyUpgrade:false', function (done) { - var server = http.createServer(); - eio.attach(server, { destroyUpgrade: false, destroyUpgradeTimeout: 50 }); - - server.listen(function () { - var client = net.createConnection(server.address().port); - client.on('connect', function () { - client.setEncoding('ascii'); - client.write([ - 'GET / HTTP/1.1', - 'Connection: Upgrade', - 'Upgrade: IRC/6.9', - '', '' - ].join('\r\n')); - - setTimeout(function () { - client.removeListener('end', onEnd); + 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')); + function onEnd() { + done(new Error("Client should not end")); } - client.on('end', onEnd); + client.on("end", onEnd); }); }); }); - it('should destroy unhandled upgrades with after a timeout', function (done) { - var server = http.createServer(); - eio.attach(server, { destroyUpgradeTimeout: 200 }); - - server.listen(function () { - var client = net.createConnection(server.address().port); - client.on('connect', function () { - client.setEncoding('ascii'); - client.write([ - 'GET / HTTP/1.1', - 'Connection: Upgrade', - 'Upgrade: IRC/6.9', - '', '' - ].join('\r\n')); + 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(function () { - client.write('foo'); + setTimeout(() => { + client.write("foo"); }, 100); - function onEnd () { + function onEnd() { done(); } - client.on('end', onEnd); + client.on("end", onEnd); }); }); }); - it('should not destroy handled upgrades with after a timeout', function (done) { - var server = http.createServer(); - eio.attach(server, { destroyUpgradeTimeout: 100 }); + 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', function (req, socket) { - socket.write('foo'); - socket.on('data', function (chunk) { - expect(chunk.toString()).to.be('foo'); + server.on("upgrade", (req, socket) => { + socket.write("foo"); + socket.on("data", chunk => { + expect(chunk.toString()).to.be("foo"); socket.end(); }); }); - server.listen(function () { - var client = net.createConnection(server.address().port); - - client.on('connect', function () { - client.setEncoding('ascii'); - client.write([ - 'GET / HTTP/1.1', - 'Connection: Upgrade', - 'Upgrade: IRC/6.9', - '', '' - ].join('\r\n')); + 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(function () { - client.write('foo'); + setTimeout(() => { + client.write("foo"); }, 200); - client.on('data', function (data) { - }); + client.on("data", data => {}); - client.on('end', done); + client.on("end", done); }); }); }); - it('should preserve original request listeners', function (done) { - var listeners = 0; - var server = http.createServer(function (req, res) { + 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', function (req, res) { + server.on("request", (req, res) => { expect(req && res).to.be.ok(); res.writeHead(200); - res.end(''); + res.end(""); listeners++; }); - eio.attach(server); - - server.listen(function () { - var port = server.address().port; - request.get('http://localhost:%d/engine.io/default/'.s(port), function (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:%d/test'.s(port), function (err, res) { - expect(err).to.be(null); - expect(res.status).to.be(200); - expect(listeners).to.eql(2); - server.once('close', done); - server.close(); - }); - }); + 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/test/fixtures/client.crt b/test/fixtures/client.crt index 7264aa0d4..2957b4abc 100644 --- a/test/fixtures/client.crt +++ b/test/fixtures/client.crt @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDojCCAYoCAQIwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV -BAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 -ZDAeFw0xNTExMTgxNzM4MDVaFw0yNTExMTUxNzM4MDVaMG0xCzAJBgNVBAYTAkZJ -MRMwEQYDVQQIEwpTb21lLVN0YXRlMREwDwYDVQQHEwhIZWxzaW5raTEhMB8GA1UE -ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRMwEQYDVQQDEwpGb28gQ2xpZW50 -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQD66Rn8P8O+MK13sPxIIEMHXDRZ -heRLqGNlNsXzaBLWnKSlV+Wxi1OdCimtAh4ZAVRt1JkK9mQEAGdxC8TRwDMS02+E -UK1H1zvh77Ek4ZcHW8p5CVEm53FTmO+jhL+7BQYXW1yi/XURBv2xm3Q95I7895ag -prMFI8HiOu/Hdi/iDQIDAQABMA0GCSqGSIb3DQEBBQUAA4ICAQBynfI9C5/zcukL -oQmUSEg5ys99bGUa8QyhSRbp5nb8Wl76KRIxpqGxl+3wcyhES3cH1Vgv4CMvrVag -Qov4TG4B30MvEvd17BMD1BmDNqjOInW72ujS9wo+2K6NASw1r/96Ay7dJ7+3HE0r -gs9yaiRL5UG4y7Sp1gy5JynXNozNowVP/cy84L9K2oyiJpyc5Owg93i3E7X+/eXd -JfFTO6ZfTGNdN+hMjleyj/LXOsoeaYg3GhO9i99nzf3u0HBXI1wRbkRPJ0aNSkH0 -VEValjOGeRdKvlZhsf7x8Kg7Nk7fyIyhmYU/tBOSUasFiB7CfKN/m0P1e6DMqihW -U/k4bzoVND3KuNH5EZmfSs3BE/w5fakObCpkxAMMm6Lxtb6qBV2HlWFfatcL2WoM -nqdCEPjct7crPbgtn3Pa/erlvmAWFoqsCu4Xn4SRGP1JtGXbCLaAW4S5CffaODRp -4urgxD8bfk9Mm/9b/xAAMNkcAi4oBikVTnVWrpul/qIhY0iMfqIu6MkbWAthMT4h -th5AggLxl6+dcPGNaliC5JpRtCis+fWuKxwbf8YKSEIwi0VBQbPP2ATinqfOJ1Yo -/tymAa3IKukil6RuTECTpE06x+Ns0rMJTSN2SGd4AG/n1/SAjhBHoH1J0J4JG30f -5fx0ax5MzGuTrt9yNEUvIrKj6MIgmw== +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/test/fixtures/client.pfx b/test/fixtures/client.pfx index 11e663341..381a0a4f2 100644 Binary files a/test/fixtures/client.pfx and b/test/fixtures/client.pfx differ diff --git a/test/fixtures/server-close-upgraded.js b/test/fixtures/server-close-upgraded.js index dc608d30b..774a21edb 100644 --- a/test/fixtures/server-close-upgraded.js +++ b/test/fixtures/server-close-upgraded.js @@ -1,9 +1,8 @@ -var eioc = require('engine.io-client'); -var listen = require('../common').listen; +const { ClientSocket, listen } = require("../common"); -var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:' + port); - socket.on('upgrade', function () { +const engine = listen(port => { + const socket = new ClientSocket("ws://localhost:" + port); + socket.on("upgrade", () => { engine.httpServer.close(); engine.close(); }); diff --git a/test/fixtures/server-close-upgrading.js b/test/fixtures/server-close-upgrading.js index 4ebfcc52b..dcf2861d6 100644 --- a/test/fixtures/server-close-upgrading.js +++ b/test/fixtures/server-close-upgrading.js @@ -1,9 +1,8 @@ -var eioc = require('engine.io-client'); -var listen = require('../common').listen; +const { ClientSocket, listen } = require("../common"); -var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:' + port); - socket.on('upgrading', function () { +const engine = listen(port => { + const socket = new ClientSocket("ws://localhost:" + port); + socket.on("upgrading", () => { engine.httpServer.close(); engine.close(); }); diff --git a/test/fixtures/server-close.js b/test/fixtures/server-close.js index 07c312094..d46b81d56 100644 --- a/test/fixtures/server-close.js +++ b/test/fixtures/server-close.js @@ -1,9 +1,8 @@ -var eioc = require('engine.io-client'); -var listen = require('../common').listen; +const { ClientSocket, listen } = require("../common"); -var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:' + port); - socket.on('open', function () { +const engine = listen(port => { + const socket = new ClientSocket("ws://localhost:" + port); + socket.on("open", () => { engine.httpServer.close(); engine.close(); }); diff --git a/test/fixtures/server.crt b/test/fixtures/server.crt index 0b49d96e5..acc6c646f 100644 --- a/test/fixtures/server.crt +++ b/test/fixtures/server.crt @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -MIIDoTCCAYkCAQEwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV -BAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 -ZDAeFw0xNTExMTgxNzM4MDhaFw0yNTExMTUxNzM4MDhaMGwxCzAJBgNVBAYTAkZJ -MRMwEQYDVQQIEwpTb21lLVN0YXRlMREwDwYDVQQHEwhIZWxzaW5raTEhMB8GA1UE -ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDEwlsb2NhbGhvc3Qw -gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMBAs08Mguowi6W8+BrqiByLr3Fh -pKnnRTtO/wfDDebgP3Da5vIapHt0NbbSIYkju1M1y/9S0uqCScSIIFeSr8wOvmB1 -c0jRX+P6oA6p8lJXG/HEfEuu3uK+Olkds7sHcrtuRaYLEAXNYCUTlT0VdfVvxV96 -0m/5wMRpexjC5h+jAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACvxt9oSl8zDz2i9 -DFtBVA87TYkv7MPqc6HKzpzKjUpay1CmTrXxaw9WKXGC47aoj/wXNqe1W/w5KUdH -79Z7n/TKpQJ9hj3NQJerFGD5EGu+rL4/FshMiwjp0/AiuAIThVa11BDzmEltwf86 -mHfIhRG89m3LmJbCzOLTjqjnt3vL5fcW0KEN+5TDFxINbXC6o+ioZgMeh3n+d01D -MZImN01A0ZwKch81IDmPZD/IrtCLGwG/YfTaQUshMHUaQIKqDIyzDb/VgWGh1vET -J6Zpbvr3xc+mg34588qd6JcyND6X1LKBvLUNqcIrcAaChbabyPUCy1w+XDgcATZg -uerXcELSApivaSVGZVNkrbUskDySjZp2NhpEhDFim3HZye7CUvVIM5AI8BPeI5ZA -nYi2kcya3e9/u3YxfsCklbxCiBUMhVYT3EVHNILr0Eik4NJp92xBLyRk770WvwMc -MAiRNK5+XM0DffzHe9PrcopWRwvPsh8ce9IUsTJQjx0zALc8JKT6VN8J1cNseZR5 -ALuTasgFliRFYCwUJcwCpoXUDJas7hbBDcbecMbFKDCrmUbG6moRqETtljDtn4na -cKwaAk2sp9CSaNoLAsWd45ElXpeoKtNz7dFsRf1nSu2GPdE2SdaPJ6br+bKcO6TW -LRObFtVqZM7TTrPUuVji72CfFMtl +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/test/jsonp.js b/test/jsonp.js deleted file mode 100644 index 776582818..000000000 --- a/test/jsonp.js +++ /dev/null @@ -1,231 +0,0 @@ - -/** - * Module dependencies. - */ - -var eioc = require('engine.io-client'); -var listen = require('./common').listen; -var expect = require('expect.js'); -var request = require('superagent'); - -describe('JSONP', function () { - before(function () { - // we have to override the browser's functionality for JSONP - document = { // eslint-disable-line no-global-assign - body: { - appendChild: function () {}, - removeChild: function () {} - } - }; - - document.createElement = function (name) { - var self = this; - - if ('script' === name) { - var script = {}; - - script.__defineGetter__('parentNode', function () { - return document.body; - }); - - script.__defineSetter__('src', function (uri) { - request.get(uri).end(function (err, res) { - expect(err).to.be(null); - eval(res.text); // eslint-disable-line no-eval - }); - }); - return script; - } else if ('form' === name) { - var form = { - style: {}, - action: '', - parentNode: { removeChild: function () {} }, - removeChild: function () {}, - setAttribute: function () {}, - appendChild: function () {}, - submit: function () { - request - .post(this.action) - .type('form') - .send({ d: self.areaValue }) - .end(function () {}); - } - }; - return form; - } else if ('textarea' === name) { - var textarea = {}; - - // a hack to be able to access the area data when form is sent - textarea.__defineSetter__('value', function (data) { - self.areaValue = data; - }); - return textarea; - } else if (~name.indexOf('iframe')) { - var iframe = {}; - setTimeout(function () { - if (iframe.onload) iframe.onload(); - }, 0); - - return iframe; - } else { - return {}; - } - }; - - document.getElementsByTagName = function (name) { - return [{ - parentNode: { - insertBefore: function () {} - } - }]; - }; - }); - - after(function () { - delete document.getElementsByTagName; - delete document.createElement; - delete global.document; - }); - - describe('handshake', function () { - it('should open with polling JSONP when requested', function (done) { - var engine = listen({ allowUpgrades: false, transports: ['polling'] }, function (port) { - eioc('ws://localhost:' + port, - { transports: ['polling'], forceJSONP: true, upgrade: false }); - engine.on('connection', function (socket) { - expect(socket.transport.name).to.be('polling'); - expect(socket.transport.head).to.be('___eio[0]('); - done(); - }); - }); - }); - }); - - describe('messages', function () { - var engine, port, socket; - - beforeEach(function (done) { - engine = listen({ allowUpgrades: false, transports: ['polling'] }, function (p) { - port = p; - - socket = new eioc.Socket('ws://localhost:' + port - , { transports: ['polling'], forceJSONP: true, upgrade: false }); - - done(); - }); - }); - - it('should arrive from client to server and back (pollingJSONP)', function (done) { - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - conn.send('a'); - }); - }); - - socket.on('open', function () { - socket.send('a'); - socket.on('message', function (msg) { - expect(socket.transport.query.j).to.not.be(undefined); - expect(msg).to.be('a'); - done(); - }); - }); - }); - - it('should not fail JSON.parse for stringified messages', function (done) { - engine.on('connection', function (conn) { - conn.on('message', function (message) { - expect(JSON.parse(message)).to.be.eql({test: 'a\r\nb\n\n\n\nc'}); - done(); - }); - }); - socket.on('open', function () { - socket.send(JSON.stringify({test: 'a\r\nb\n\n\n\nc'})); - }); - }); - - it('should parse newlines in message correctly', function (done) { - engine.on('connection', function (conn) { - conn.on('message', function (message) { - expect(message).to.be.equal('a\r\nb\n\n\n\nc'); - done(); - }); - }); - socket.on('open', function () { - socket.send('a\r\nb\n\n\n\nc'); - }); - }); - - it('should arrive from server to client and back with binary data (pollingJSONP)', function (done) { - var binaryData = Buffer.allocUnsafe(5); - for (var i = 0; i < 5; i++) binaryData[i] = i; - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - conn.send(msg); - }); - }); - - socket.on('open', function () { - socket.send(binaryData); - socket.on('message', function (msg) { - for (var i = 0; i < msg.length; i++) expect(msg[i]).to.be(i); - done(); - }); - }); - }); - }); - - describe('close', function () { - it('should trigger when server closes a client', function (done) { - var engine = listen({ allowUpgrades: false, transports: ['polling'] }, function (port) { - var socket = new eioc.Socket('ws://localhost:' + port, - { transports: ['polling'], forceJSONP: true, upgrade: false }); - var total = 2; - - engine.on('connection', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('forced close'); - --total || done(); - }); - setTimeout(function () { - conn.close(); - }, 10); - }); - - socket.on('open', function () { - socket.on('close', function (reason) { - expect(reason).to.be('transport close'); - --total || done(); - }); - }); - }); - }); - - it('should trigger when client closes', function (done) { - var engine = listen({ allowUpgrades: false, transports: ['polling'] }, function (port) { - var socket = new eioc.Socket('ws://localhost:' + port - , { transports: ['polling'], forceJSONP: true, upgrade: false }); - var total = 2; - - engine.on('connection', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('transport close'); - --total || done(); - }); - }); - - socket.on('open', function () { - socket.send('a'); - socket.on('close', function (reason) { - expect(reason).to.be('forced close'); - --total || done(); - }); - - setTimeout(function () { - socket.close(); - }, 10); - }); - }); - }); - }); -}); diff --git a/test/parser.js b/test/parser.js new file mode 100644 index 000000000..3f967e1b7 --- /dev/null +++ b/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/test/server.js b/test/server.js index 9da4d109b..74ad2a17b 100644 --- a/test/server.js +++ b/test/server.js @@ -1,238 +1,354 @@ /* eslint-disable standard/no-callback-literal */ -/** - * Tests dependencies. - */ - -var http = require('http'); -var https = require('https'); -var fs = require('fs'); -var path = require('path'); -var exec = require('child_process').exec; -var zlib = require('zlib'); -var eio = require('..'); -var eioc = require('engine.io-client'); -var listen = require('./common').listen; -var expect = require('expect.js'); -var request = require('superagent'); -var cookieMod = require('cookie'); - -// are we running on node < 4.4.3 ? -var NODE_LT_443 = (function () { - var parts = process.versions.node.split('.'); - return (parts[0] < 4 || parts[1] < 4 || parts[2] < 3); -})(); -// are we running uws wsEngine ? -var UWS_ENGINE = process.env.EIO_WS_ENGINE === 'uws'; +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"); /** * Tests. */ -describe('server', function () { - describe('verification', function () { - it('should disallow non-existent transports', function (done) { - listen(function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'tobi' }) // no tobi transport - outrageous - .end(function (err, res) { +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'); - expect(res.header['access-control-allow-origin']).to.be('*'); - done(); + expect(res.body.message).to.be("Transport unknown"); + partialDone(); }); }); }); - it('should disallow `constructor` as transports', function (done) { + it("should disallow `constructor` as transports", done => { + const partialDone = createPartialDone(done, 2); + // make sure we check for actual properties - not those present on every {} - listen(function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'constructor' }) - .end(function (err, res) { + 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'); - expect(res.header['access-control-allow-credentials']).to.be('true'); - expect(res.header['access-control-allow-origin']).to.be('http://engine.io'); - done(); + expect(res.body.message).to.be("Transport unknown"); + partialDone(); }); }); }); - it('should disallow non-existent sids', function (done) { - listen(function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'polling', sid: 'test' }) - .end(function (err, res) { + 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'); - expect(res.header['access-control-allow-credentials']).to.be('true'); - expect(res.header['access-control-allow-origin']).to.be('http://engine.io'); - done(); + expect(res.body.message).to.be("Session ID unknown"); + partialDone(); }); }); }); - it('should disallow requests that are rejected by `allowRequest`', function (done) { - listen({ allowRequest: function (req, fn) { fn('Thou shall not pass', false); } }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'polling' }) - .end(function (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'); - expect(res.header['access-control-allow-credentials']).to.be(undefined); - expect(res.header['access-control-allow-origin']).to.be(undefined); + 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 disallow connection that are rejected by `allowRequest`', function (done) { - listen({ allowRequest: function (req, fn) { fn(null, false); } }, function (port) { - var client = eioc('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - client.on('error', function () { - done(); - }); + 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); + }); }); }); }); - describe('handshake', function () { - it('should send the io cookie', function (done) { - listen(function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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 - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid + '; Path=/; HttpOnly'); + 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', function (done) { - listen({ cookie: 'woot' }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('woot=' + sid + '; Path=/; HttpOnly'); + 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', function (done) { - listen({ cookiePath: '/custom' }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid + '; Path=/custom; HttpOnly'); + 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', function (done) { - listen({ cookiePath: false }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid); + 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', function (done) { - listen({ cookieHttpOnly: true }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid + '; Path=/; HttpOnly'); + 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 httpOnly=true and path=false', function (done) { - listen({ cookieHttpOnly: true, cookiePath: false }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid); + 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', function (done) { - listen({ cookieHttpOnly: false }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid + '; Path=/'); + 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', function (done) { - listen({ cookieHttpOnly: 'no' }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling', b64: 1 }) - .end(function (err, res) { + 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); - var sid = res.text.match(/"sid":"([^"]+)"/)[1]; - expect(res.headers['set-cookie'][0]).to.be('io=' + sid + '; Path=/; HttpOnly'); + 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', function (done) { - listen({ cookie: false }, function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .query({ transport: 'polling' }) - .end(function (err, res) { + 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); + expect(res.headers["set-cookie"]).to.be(undefined); done(); }); }); }); - it('should register a new client', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { + 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); - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { + 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(); @@ -240,19 +356,17 @@ describe('server', function () { }); }); - it('should register a new client with custom id', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { + 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); - var customId = 'CustomId' + Date.now(); + const customId = "CustomId" + Date.now(); - engine.generateId = function (req) { - return customId; - }; + engine.generateId = req => customId; - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.once('open', function () { + 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); @@ -262,172 +376,370 @@ describe('server', function () { }); }); - it('should exchange handshake data', function (done) { - listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('handshake', function (obj) { - expect(obj.sid).to.be.a('string'); - expect(obj.pingTimeout).to.be.a('number'); - expect(obj.upgrades).to.be.an('array'); + 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', function (done) { - listen({ allowUpgrades: false, pingTimeout: 123 }, function (port) { - var socket = new eioc.Socket('http://localhost:%d'.s(port)); - socket.on('handshake', function (obj) { + 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', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - eioc('ws://localhost:%d'.s(port)); - engine.on('connection', function (socket) { - expect(socket).to.be.an(eio.Socket); + 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', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - eioc('ws://localhost:%d'.s(port)); - engine.on('connection', function (socket) { - expect(socket.transport.name).to.be('polling'); + 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', function (done) { - var engine = listen({ transports: ['websocket'] }, function (port) { - eioc('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - engine.on('connection', function (socket) { - expect(socket.transport.name).to.be('websocket'); + 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', function (done) { - listen({ transports: ['websocket'] }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - socket.on('handshake', function (obj) { + 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', function (done) { - listen({ transports: ['polling'] }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { }); - socket.on('handshake', function (obj) { + 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', function (done) { - listen({ transports: ['polling'] }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { }); - socket.on('handshake', function (obj) { + 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', function (done) { - listen({}, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { }); - socket.on('handshake', function (obj) { + 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'); + expect(obj.upgrades).to.have.contain("websocket"); done(); }); }); }); - it('default to polling when proxy doesn\'t support websocket', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - engine.on('connection', function (socket) { - socket.on('message', function (msg) { - if ('echo' === msg) socket.send(msg); + 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); }); }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { - request.get('http://localhost:%d/engine.io/'.s(port)) - .set({ connection: 'close' }) - .query({ transport: 'websocket', sid: socket.id }) - .end(function (err, res) { + 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', function (msg) { - expect(msg).to.be('echo'); - done(); + socket.send("echo"); + socket.on("message", msg => { + expect(msg).to.be("echo"); + partialDone(); }); }); }); }); }); - it('should allow arbitrary data through query string', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - eioc('ws://localhost:%d'.s(port), { query: { a: 'b' } }); - engine.on('connection', function (conn) { - expect(conn.request._query).to.have.keys('transport', 'a'); - expect(conn.request._query.a).to.be('b'); + 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', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - eioc('ws://localhost:%d?a=b&c=d'.s(port)); - engine.on('connection', function (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'); + 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', function (done) { - listen(function (port) { - request.get('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'websocket' }) - .end(function (err, res) { + 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'); - expect(res.header['access-control-allow-credentials']).to.be('true'); - expect(res.header['access-control-allow-origin']).to.be('http://engine.io'); - done(); + expect(res.body.message).to.be("Bad request"); + partialDone(); }); }); }); - it('should send a packet along with the handshake', function (done) { - listen({ initialPacket: 'faster!' }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { - socket.on('message', function (msg) { - expect(msg).to.be('faster!'); + 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(); }); }); @@ -435,68 +747,73 @@ describe('server', function () { }); }); - describe('close', function () { - it('should be able to access non-empty writeBuffer at closing (server)', function (done) { - var opts = {allowUpgrades: false}; - var engine = listen(opts, function (port) { - eioc('http://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('close', function (reason) { + 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(function () { + setTimeout(() => { expect(conn.writeBuffer.length).to.be(0); // writeBuffer has been cleared }, 10); done(); }); - conn.writeBuffer.push({ type: 'message', data: 'foo' }); - conn.onError(''); + conn.writeBuffer.push({ type: "message", data: "foo" }); + conn.onError(""); }); }); }); - it('should be able to access non-empty writeBuffer at closing (client)', function (done) { - var opts = {allowUpgrades: false}; - listen(opts, function (port) { - var socket = new eioc.Socket('http://localhost:%d'.s(port)); - socket.on('open', function () { - socket.on('close', function (reason) { + 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(function () { + setTimeout(() => { expect(socket.writeBuffer.length).to.be(0); }, 10); done(); }); - socket.writeBuffer.push({ type: 'message', data: 'foo' }); - socket.onError(''); + socket.writeBuffer.push({ type: "message", data: "foo" }); + socket.onError(""); }); }); }); - it('should trigger on server if the client does not pong', function (done) { - var opts = { allowUpgrades: false, pingInterval: 5, pingTimeout: 5 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('http://localhost:%d'.s(port)); - socket.sendPacket = function () {}; - engine.on('connection', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('ping timeout'); + 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)', function (done) { - var opts = { allowUpgrades: false, pingInterval: 500, pingTimeout: 500 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('http://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('ping timeout'); + 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.onPacket = function () {}; + socket.sendPacket = () => {}; + socket.transport.removeListener("packet"); socket.close(); // then server app tries to close the socket, since client disappeared conn.close(); @@ -504,441 +821,550 @@ describe('server', function () { }); }); - it('should trigger on client if server does not meet ping timeout', function (done) { - var opts = { allowUpgrades: false, pingInterval: 50, pingTimeout: 30 }; - listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { + 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.onPacket = function () {}; - socket.transport.onClose = function () {}; - socket.on('close', function (reason, err) { - expect(reason).to.be('ping timeout'); + 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', function (done) { - var opts = { allowUpgrades: false, pingTimeout: 50, pingInterval: 50 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var total = 2; + 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'); + function onClose(reason, err) { + expect(reason).to.be("ping timeout"); --total || done(); } - engine.on('connection', function (conn) { - conn.on('close', onClose); + engine.on("connection", conn => { + conn.on("close", onClose); }); - socket.on('open', function () { + socket.on("open", () => { // override onPacket and Transport#onClose to simulate an inactive server after handshake - socket.onPacket = socket.sendPacket = function () {}; - socket.transport.onClose = function () {}; - socket.on('close', onClose); + socket.sendPacket = () => {}; + socket.transport.removeListener("packet"); + socket.transport.removeListener("close"); + socket.on("close", onClose); }); }); }); - it('should trigger when server closes a client', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var total = 2; + 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', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('forced close'); + engine.on("connection", conn => { + conn.on("close", reason => { + expect(reason).to.be("forced close"); --total || done(); }); - setTimeout(function () { + setTimeout(() => { conn.close(); }, 10); }); - socket.on('open', function () { - socket.on('close', function (reason) { - expect(reason).to.be('transport close'); + socket.on("open", () => { + socket.on("close", reason => { + expect(reason).to.be("transport close"); --total || done(); }); }); }); }); - it('should trigger when server closes a client (ws)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var total = 2; + 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', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('forced close'); + engine.on("connection", conn => { + conn.on("close", reason => { + expect(reason).to.be("forced close"); --total || done(); }); - setTimeout(function () { + setTimeout(() => { conn.close(); }, 10); }); - socket.on('open', function () { - socket.on('close', function (reason) { - expect(reason).to.be('transport close'); + 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) { - var opts = { transports: ['websocket'] }; - var engine = listen(opts, function (port) { + 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); - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"] + }); - engine.once('connection', function (conn) { - setTimeout(function () { + engine.once("connection", conn => { + setTimeout(() => { conn.close(); }, 10); }); - socket.once('close', function (reason) { - expect(reason).to.be('transport close'); + socket.once("close", reason => { + expect(reason).to.be("transport close"); done(); }); }); }); - it('should trigger when client closes', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var total = 2; + 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', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('transport close'); + engine.on("connection", conn => { + conn.on("close", reason => { + expect(reason).to.be("transport close"); --total || done(); }); }); - socket.on('open', function () { - socket.on('close', function (reason) { - expect(reason).to.be('forced close'); + socket.on("open", () => { + socket.on("close", reason => { + expect(reason).to.be("forced close"); --total || done(); }); - setTimeout(function () { + setTimeout(() => { socket.close(); }, 10); }); }); }); - it('should trigger when client closes (ws)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var total = 2; + 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', function (conn) { - conn.on('close', function (reason) { - expect(reason).to.be('transport close'); + engine.on("connection", conn => { + conn.on("close", reason => { + expect(reason).to.be("transport close"); --total || done(); }); }); - socket.on('open', function () { - socket.on('close', function (reason) { - expect(reason).to.be('forced close'); + socket.on("open", () => { + socket.on("close", reason => { + expect(reason).to.be("forced close"); --total || done(); }); - setTimeout(function () { + setTimeout(() => { socket.close(); }, 10); }); }); }); - it('should trigger when calling socket.close() in payload', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); + 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', function (conn) { - conn.send(null, function () { socket.close(); }); - conn.send('this should not be handled'); + engine.on("connection", conn => { + conn.send(null, () => { + socket.close(); + }); + conn.send("this should not be handled"); - conn.on('close', function (reason) { - expect(reason).to.be('transport close'); + conn.on("close", reason => { + expect(reason).to.be("transport close"); done(); }); }); - socket.on('open', function () { - socket.on('message', function (msg) { - expect(msg).to.not.be('this should not be handled'); + socket.on("open", () => { + socket.on("message", msg => { + expect(msg).to.not.be("this should not be handled"); }); - socket.on('close', function (reason) { - expect(reason).to.be('forced close'); + socket.on("close", reason => { + expect(reason).to.be("forced close"); }); }); }); }); - it('should abort upgrade if socket is closed (GH-35)', function (done) { - listen({ allowUpgrades: true }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { + 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(function () { + setTimeout(() => { done(); }, 100); }); }); }); - it('should trigger if a poll request is ongoing and the underlying ' + - 'socket closes, as in a browser tab close', function ($done) { - var engine = listen({ allowUpgrades: false }, function (port) { - // hack to access the sockets created by node-xmlhttprequest - // see: https://github.com/driverdan/node-XMLHttpRequest/issues/44 - var request = require('http').request; - var sockets = []; - http.request = function (opts) { - var req = request.apply(null, arguments); - req.on('socket', function (socket) { - sockets.push(socket); - }); - return req; - }; + 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(); - } + function done() { + http.request = request; + $done(); + } - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var serverSocket; + var socket = new ClientSocket(`ws://localhost:${port}`); + let serverSocket; - engine.on('connection', function (s) { - serverSocket = s; - }); + engine.on("connection", s => { + serverSocket = s; + }); - socket.transport.on('poll', function () { - // we set a timer to wait for the request to actually reach - setTimeout(function () { - // at this time server's `connection` should have been fired - expect(serverSocket).to.be.an('object'); + 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); + // 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); + // 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'); + // 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', function (reason, err) { - expect(reason).to.be('transport error'); - expect(err.message).to.be('poll connection closed prematurely'); - done(); - }); - }, 50); + // 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', function ($done) { - var engine = listen({ allowUpgrades: false }, function (port) { + it("should not trigger with connection: close header", $done => { + const engine = listen({ allowUpgrades: false }, port => { // intercept requests to add connection: close - var request = http.request; - http.request = function () { - var opts = arguments[0]; + const request = http.request; + http.request = function() { + const opts = arguments[0]; opts.headers = opts.headers || {}; - opts.headers.Connection = 'close'; + opts.headers.Connection = "close"; return request.apply(this, arguments); }; - function done () { + function done() { http.request = request; $done(); } - engine.on('connection', function (socket) { - socket.on('message', function (msg) { - expect(msg).to.equal('test'); - socket.send('woot'); + engine.on("connection", socket => { + socket.on("message", msg => { + expect(msg).to.equal("test"); + socket.send("woot"); }); }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { - socket.send('test'); + var socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("open", () => { + socket.send("test"); }); - socket.on('message', function (msg) { - expect(msg).to.be('woot'); + socket.on("message", msg => { + expect(msg).to.be("woot"); done(); }); }); }); - it('should not trigger early with connection `ping timeout`' + - 'after post handshake timeout', function (done) { - // first timeout should trigger after `pingInterval + pingTimeout`, - // not just `pingTimeout`. - var opts = { allowUpgrades: false, pingInterval: 300, pingTimeout: 100 }; - listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var clientCloseReason = null; + 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', function () { - socket.onPacket = function () {}; - }); - socket.on('open', function () { - socket.on('close', function (reason) { - clientCloseReason = reason; + socket.on("handshake", () => { + socket.transport.removeListener("packet"); }); - }); - - setTimeout(function () { - expect(clientCloseReason).to.be(null); - done(); - }, 200); - }); - }); - - it('should not trigger early with connection `ping timeout` ' + - 'after post ping timeout', function (done) { - // ping timeout should trigger after `pingInterval + pingTimeout`, - // not just `pingTimeout`. - var opts = { allowUpgrades: false, pingInterval: 80, pingTimeout: 50 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var clientCloseReason = null; - - engine.on('connection', function (conn) { - conn.on('heartbeat', function () { - conn.onPacket = function () {}; + 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; - socket.on('open', function () { - socket.on('close', function (reason) { - clientCloseReason = reason; + engine.on("connection", conn => { + conn.on("heartbeat", () => { + conn.onPacket = () => {}; + }); }); - }); - setTimeout(function () { - expect(clientCloseReason).to.be(null); - done(); - }, 100); - }); - }); + socket.on("open", () => { + socket.on("close", reason => { + clientCloseReason = reason; + }); + }); - it('should trigger early with connection `transport close` ' + - 'after missing pong', function (done) { - // ping timeout should trigger after `pingInterval + pingTimeout`, - // not just `pingTimeout`. - var opts = { allowUpgrades: false, pingInterval: 80, pingTimeout: 50 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var clientCloseReason = null; + 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', function () { - socket.on('close', function (reason) { - clientCloseReason = reason; + socket.on("open", () => { + socket.on("close", reason => { + clientCloseReason = reason; + }); }); - }); - engine.on('connection', function (conn) { - conn.on('heartbeat', function () { - setTimeout(function () { - conn.close(); - }, 20); - setTimeout(function () { - expect(clientCloseReason).to.be('transport close'); - done(); - }, 100); + 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; - it('should trigger with connection `ping timeout` ' + - 'after `pingInterval + pingTimeout`', function (done) { - var opts = { allowUpgrades: false, pingInterval: 300, pingTimeout: 100 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var clientCloseReason = null; + socket.on("open", () => { + socket.on("close", reason => { + clientCloseReason = reason; + }); + }); - socket.on('open', function () { - socket.on('close', function (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; - engine.on('connection', function (conn) { - conn.once('heartbeat', function () { - setTimeout(function () { - socket.onPacket = function () {}; - expect(clientCloseReason).to.be(null); - }, 150); - setTimeout(function () { - expect(clientCloseReason).to.be(null); - }, 350); - setTimeout(function () { - expect(clientCloseReason).to.be('ping timeout'); - done(); - }, 500); - }); - }); - }); - }); + socket.on("open", () => { + socket.on("close", reason => { + clientCloseReason = reason; + }); + }); - it('should abort the polling data request if it is ' + - 'in progress', function (done) { - var engine = listen({ transports: [ 'polling' ] }, function (port) { - var socket = new eioc.Socket('http://localhost:%d'.s(port)); + 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); + }); + }); + }); + } + ); + } - engine.on('connection', function (conn) { - var onDataRequest = conn.transport.onDataRequest; - conn.transport.onDataRequest = function (req, res) { - engine.httpServer.close(done); - onDataRequest.call(conn.transport, req, res); - req.removeAllListeners(); - conn.close(); - }; - }); + 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', function () { - socket.send('test'); + 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', function (done) { - var opts = { transports: ['websocket'] }; - listen(opts, function (port) { - var url = 'ws://%s:%d'.s('0.0.0.50', port); - var socket = new eioc.Socket(url); - socket.on('open', function () { - done(new Error('Test invalidation')); - }); - socket.on('close', function (reason) { - expect(reason).to.be('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(); }); }); @@ -946,15 +1372,15 @@ describe('server', function () { // tests https://github.com/LearnBoost/engine.io-client/issues/207 // polling test, transport error - it('should trigger transport close before open for xhr', function (done) { - var opts = { transports: ['polling'] }; - listen(opts, function (port) { - var socket = new eioc.Socket('http://invalidserver:%d'.s(port)); - socket.on('open', function () { - done(new Error('Test invalidation')); - }); - socket.on('close', function (reason) { - expect(reason).to.be('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(); }); }); @@ -962,15 +1388,15 @@ describe('server', function () { // tests https://github.com/LearnBoost/engine.io-client/issues/207 // websocket test, force close - it('should trigger force close before open for ws', function (done) { - var opts = { transports: ['websocket'] }; - listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { - done(new Error('Test invalidation')); - }); - socket.on('close', function (reason) { - expect(reason).to.be('forced 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(); @@ -979,334 +1405,401 @@ describe('server', function () { // tests https://github.com/LearnBoost/engine.io-client/issues/207 // polling test, force close - it('should trigger force close before open for xhr', function (done) { - var opts = { transports: ['polling'] }; - listen(opts, function (port) { - var socket = new eioc.Socket('http://localhost:%d'.s(port)); - socket.on('open', function () { - done(new Error('Test invalidation')); - }); - socket.on('close', function (reason) { - expect(reason).to.be('forced 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)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'], pingInterval: 50, pingTimeout: 30 }; - var engine = listen(opts, function (port) { - engine.on('connection', function (conn) { - conn.transport.on('close', done); + 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"] }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); // override to simulate an inactive client - socket.sendPacket = socket.onHeartbeat = function () {}; + socket.sendPacket = socket.onHeartbeat = () => {}; }); }); - it('should close transport upon ping timeout (polling)', function (done) { - var opts = { allowUpgrades: false, transports: ['polling'], pingInterval: 50, pingTimeout: 30 }; - var engine = listen(opts, function (port) { - engine.on('connection', function (conn) { - conn.transport.on('close', done); + 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"] }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); // override to simulate an inactive client - socket.sendPacket = socket.onHeartbeat = function () {}; + socket.sendPacket = socket.onHeartbeat = () => {}; }); }); - it('should close transport upon parse error (ws)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - engine.on('connection', function (conn) { - conn.transport.on('close', done); + 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"] }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - socket.on('open', function () { - socket.transport.ws.send('invalid'); + socket.on("open", () => { + socket.transport.ws.send("invalid"); }); }); }); - it('should close transport upon parse error (polling)', function (done) { - var opts = { allowUpgrades: false, transports: ['polling'] }; - var engine = listen(opts, function (port) { - engine.on('connection', function (conn) { + 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); + conn.transport.on("close", done); }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - socket.on('open', function () { - socket.transport.doWrite('invalid', function () {}); + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"] + }); + socket.on("open", () => { + socket.transport.doWrite("invalid", () => {}); }); }); }); - it('should close upgrading transport upon socket close', function (done) { - var engine = listen(function (port) { - engine.on('connection', function (conn) { - conn.on('upgrading', function (transport) { - transport.on('close', done); + 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(); }); }); - eioc('ws://localhost:%d'.s(port)); + new ClientSocket(`ws://localhost:${port}`); }); }); - it('should close upgrading transport upon upgrade timeout', function (done) { - var opts = { upgradeTimeout: 100 }; - var engine = listen(opts, function (port) { - engine.on('connection', function (conn) { - conn.on('upgrading', function (transport) { - transport.on('close', done); + 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); }); }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('upgrading', function (transport) { + const socket = new ClientSocket(`ws://localhost:${port}`); + socket.on("upgrading", transport => { // override not to complete upgrading - transport.send = function () {}; + transport.send = () => {}; }); }); }); - it('should not crash when messing with Object prototype', function (done) { - Object.prototype.foo = 'bar'; // eslint-disable-line no-extend-native - var engine = listen({ allowUpgrades: true }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { + 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(function () { + setTimeout(() => { done(); }, 100); }); }); }); - describe('graceful close', function () { - function fixture (filename) { - return process.execPath + ' ' + - path.join(__dirname, 'fixtures', filename); + 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', function (done) { - exec(fixture('server-close.js'), done); + it("should stop socket and timers", done => { + exec(fixture("server-close.js"), done); }); - it('should stop upgraded socket and timers', function (done) { - exec(fixture('server-close-upgraded.js'), done); + it("should stop upgraded socket and timers", done => { + exec(fixture("server-close-upgraded.js"), done); }); - it('should stop upgrading socket and timers', function (done) { - exec(fixture('server-close-upgrading.js'), done); + it("should stop upgrading socket and timers", done => { + exec(fixture("server-close-upgrading.js"), done); }); }); }); - describe('messages', function () { + describe("messages", function() { this.timeout(5000); - it('should arrive from server to client', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.send('a'); + 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', function () { - socket.on('message', function (msg) { - expect(msg).to.be('a'); + socket.on("open", () => { + socket.on("message", msg => { + expect(msg).to.be("a"); done(); }); }); }); }); - it('should arrive from server to client (multiple)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var expected = ['a', 'b', 'c']; - var i = 0; + 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', function (conn) { - conn.send('a'); + engine.on("connection", conn => { + conn.send("a"); // we use set timeouts to ensure the messages are delivered as part // of different. - setTimeout(function () { - conn.send('b'); + setTimeout(() => { + conn.send("b"); - setTimeout(function () { + setTimeout(() => { // here we make sure we buffer both the close packet and // a regular packet - conn.send('c'); + conn.send("c"); conn.close(); }, 50); }, 50); - conn.on('close', function () { + conn.on("close", () => { // since close fires right after the buffer is drained - setTimeout(function () { + setTimeout(() => { expect(i).to.be(3); done(); }, 50); }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + 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', function (done) { - var opts = { allowUpgrades: false, transports: ['polling'], maxHttpBufferSize: 5 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - done(new Error('Test invalidation (message is longer than allowed)')); + 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', function () { - socket.send('aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha'); + socket.on("open", () => { + socket.send("aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha"); }); - socket.on('close', function () { + socket.on("close", () => { done(); }); }); }); - it('should not be receiving data when getting a message longer than maxHttpBufferSize (websocket)', function (done) { - var opts = { maxHttpBufferSize: 5 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - done(new Error('Test invalidation (message is longer than allowed)')); + 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', function () { - socket.send('aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha'); + socket.on("open", () => { + socket.send("aasdasdakjhasdkjhasdkjhasdkjhasdkjhasdkjhasdkjha"); }); - socket.on('close', function () { + socket.on("close", () => { done(); }); }); }); - it('should receive data when getting a message shorter than maxHttpBufferSize when polling', function (done) { - var opts = { allowUpgrades: false, transports: ['polling'], maxHttpBufferSize: 5 }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - expect(msg).to.be('a'); + 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', function () { - socket.send('a'); + socket.on("open", () => { + socket.send("a"); }); }); }); - it('should arrive from server to client (ws)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - engine.on('connection', function (conn) { - conn.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', function () { - socket.on('message', function (msg) { - expect(msg).to.be('a'); + socket.on("open", () => { + socket.on("message", msg => { + expect(msg).to.be("a"); done(); }); }); }); }); - it('should arrive from server to client (multiple, ws)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var expected = ['a', 'b', 'c']; - var i = 0; + 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', function (conn) { - conn.send('a'); - setTimeout(function () { - conn.send('b'); - setTimeout(function () { - conn.send('c'); + engine.on("connection", conn => { + conn.send("a"); + setTimeout(() => { + conn.send("b"); + setTimeout(() => { + conn.send("c"); conn.close(); }, 50); }, 50); - conn.on('close', function () { - setTimeout(function () { + conn.on("close", () => { + setTimeout(() => { expect(i).to.be(3); done(); }, 50); }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { expect(msg).to.be(expected[i++]); }); }); }); }); - it('should arrive from server to client (multiple, no delay, ws)', function (done) { - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var expected = ['a', 'b', 'c']; - var i = 0; + 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', function (conn) { - conn.on('close', function () { - setTimeout(function () { + 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(); + conn.send("a"); + conn.send("b"); + conn.send("c"); + setTimeout(() => { + conn.close(); + }, 50); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { expect(msg).to.be(expected[i++]); }); }); }); }); - it('should arrive when binary data is sent as Int8Array (ws)', function (done) { - var binaryData = new Int8Array(5); - for (var i = 0; i < binaryData.length; 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; } - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, port => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"] + }); - engine.on('connection', function (conn) { + engine.on("connection", conn => { conn.send(binaryData); }); - socket.on('open', function () { - socket.on('message', function (msg) { - for (var i = 0; i < binaryData.length; i++) { - var num = msg.readInt8(i); + 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(); @@ -1315,24 +1808,28 @@ describe('server', function () { }); }); - it('should arrive when binary data is sent as Int32Array (ws)', function (done) { - var binaryData = new Int32Array(5); - for (var i = 0; i < binaryData.length; i++) { + 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; } - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, port => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"] + }); - engine.on('connection', function (conn) { + engine.on("connection", conn => { conn.send(binaryData); }); - socket.on('open', function () { - socket.on('message', function (msg) { - for (var i = 0, ii = 0; ii < binaryData.length; i += 4, ii++) { - var num = msg.readInt32LE(i); + 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(); @@ -1341,24 +1838,28 @@ describe('server', function () { }); }); - it('should arrive when binary data is sent as Int32Array, given as ArrayBuffer(ws)', function (done) { - var binaryData = new Int32Array(5); - for (var i = 0; i < binaryData.length; i++) { + 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; } - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, port => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"] + }); - engine.on('connection', function (conn) { + engine.on("connection", conn => { conn.send(binaryData.buffer); }); - socket.on('open', function () { - socket.on('message', function (msg) { - for (var i = 0, ii = 0; ii < binaryData.length; i += 4, ii++) { - var num = msg.readInt32LE(i); + 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(); @@ -1367,24 +1868,26 @@ describe('server', function () { }); }); - it('should arrive when binary data is sent as Buffer (ws)', function (done) { - var binaryData = Buffer.allocUnsafe(5); - for (var i = 0; i < binaryData.length; i++) { + 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); } - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); + const opts = { allowUpgrades: false, transports: ["websocket"] }; + const engine = listen(opts, port => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"] + }); - engine.on('connection', function (conn) { + engine.on("connection", conn => { conn.send(binaryData); }); - socket.on('open', function () { - socket.on('message', function (msg) { - for (var i = 0; i < binaryData.length; i++) { - var num = msg.readInt8(i); + 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(); @@ -1393,24 +1896,26 @@ describe('server', function () { }); }); - it('should arrive when binary data sent as Buffer (polling)', function (done) { - var binaryData = Buffer.allocUnsafe(5); - for (var i = 0; i < binaryData.length; i++) { + 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); } - var opts = { allowUpgrades: false, transports: ['polling'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); + const opts = { allowUpgrades: false, transports: ["polling"] }; + const engine = listen(opts, port => { + const socket = new ClientSocket(`ws://localhost:${port}`, { + transports: ["polling"] + }); - engine.on('connection', function (conn) { + engine.on("connection", conn => { conn.send(binaryData); }); - socket.on('open', function () { - socket.on('message', function (msg) { - for (var i = 0; i < binaryData.length; i++) { - var num = msg.readInt8(i); + 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); } @@ -1420,26 +1925,28 @@ describe('server', function () { }); }); - it('should arrive as ArrayBuffer if requested when binary data sent as Buffer (ws)', function (done) { - var binaryData = Buffer.allocUnsafe(5); - for (var i = 0; i < binaryData.length; i++) { + 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); } - var opts = { allowUpgrades: false, transports: ['websocket'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - socket.binaryType = 'arraybuffer'; + 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', function (conn) { + engine.on("connection", conn => { conn.send(binaryData); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { expect(msg instanceof ArrayBuffer).to.be(true); - var intArray = new Int8Array(msg); - for (var i = 0; i < binaryData.length; i++) { + const intArray = new Int8Array(msg); + for (let i = 0; i < binaryData.length; i++) { expect(intArray[i]).to.be(i); } @@ -1449,26 +1956,87 @@ describe('server', function () { }); }); - it('should arrive as ArrayBuffer if requested when binary data sent as Buffer (polling)', function (done) { - var binaryData = Buffer.allocUnsafe(5); - for (var i = 0; i < binaryData.length; i++) { + 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); } - var opts = { allowUpgrades: false, transports: ['polling'] }; - var engine = listen(opts, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - socket.binaryType = 'arraybuffer'; + 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', function (conn) { + engine.on("connection", conn => { conn.send(binaryData); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { expect(msg instanceof ArrayBuffer).to.be(true); - var intArray = new Int8Array(msg); - for (var i = 0; i < binaryData.length; i++) { + const intArray = new Int8Array(msg); + for (let i = 0; i < binaryData.length; i++) { expect(intArray[i]).to.be(i); } @@ -1478,101 +2046,109 @@ describe('server', function () { }); }); - it('should trigger a flush/drain event', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - engine.on('connection', function (socket) { - var totalEvents = 4; + it("should trigger a flush/drain event", done => { + const engine = listen({ allowUpgrades: false }, port => { + engine.on("connection", socket => { + let totalEvents = 4; - engine.on('flush', function (sock, buf) { + engine.on("flush", (sock, buf) => { expect(sock).to.be(socket); - expect(buf).to.be.an('array'); + expect(buf).to.be.an("array"); --totalEvents || done(); }); - socket.on('flush', function (buf) { - expect(buf).to.be.an('array'); + socket.on("flush", buf => { + expect(buf).to.be.an("array"); --totalEvents || done(); }); - engine.on('drain', function (sock) { + engine.on("drain", sock => { expect(sock).to.be(socket); expect(socket.writeBuffer.length).to.be(0); --totalEvents || done(); }); - socket.on('drain', function () { + socket.on("drain", () => { expect(socket.writeBuffer.length).to.be(0); --totalEvents || done(); }); - socket.send('aaaa'); + socket.send("aaaa"); }); - eioc('ws://localhost:%d'.s(port)); + new ClientSocket(`ws://localhost:${port}`); }); }); - it('should interleave with pongs if many messages buffered ' + - 'after connection open', function (done) { - this.slow(4000); - this.timeout(8000); + 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); - var opts = { - transports: ['websocket'], - pingInterval: 200, - pingTimeout: 100 - }; + const opts = { + transports: ["websocket"], + pingInterval: 200, + pingTimeout: 100 + }; - var engine = listen(opts, function (port) { - var messageCount = 100; - var messagePayload = new Array(256 * 256).join('a'); - var connection = null; - engine.on('connection', function (conn) { - connection = conn; - }); - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - socket.on('open', function () { - for (var i = 0; i < messageCount; i++) { - // connection.send('message: ' + i); // works - connection.send(messagePayload + '|message: ' + i); // does not work - } - var receivedCount = 0; - socket.on('message', function (msg) { - receivedCount += 1; - if (receivedCount === messageCount) { - done(); + 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', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var shi = '石室詩士施氏,嗜獅,誓食十獅。'; - var shi2 = '氏時時適市視獅。'; - engine.on('connection', function (conn) { - conn.send('.'); + } + ); + + 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', function (msg0) { - expect(msg0).to.be('.'); - conn.once('message', function (msg) { + conn.once("message", msg0 => { + expect(msg0).to.be("."); + conn.once("message", msg => { expect(msg).to.be(shi); - conn.once('message', function (msg2) { + conn.once("message", msg2 => { expect(msg2).to.be(shi2); done(); }); }); }); }); - socket.on('open', function () { - socket.once('message', function (msg0) { - expect(msg0).to.be('.'); - socket.once('message', function (msg) { + socket.on("open", () => { + socket.once("message", msg0 => { + expect(msg0).to.be("."); + socket.once("message", msg => { expect(msg).to.be(shi); - socket.once('message', function (msg2) { + socket.once("message", msg2 => { expect(msg2).to.be(shi2); - socket.send('.'); + socket.send("."); socket.send(shi); socket.send(shi2); }); @@ -1582,237 +2158,272 @@ describe('server', function () { }); }); - it('should send and receive data with key and cert (polling)', function (done) { - if (UWS_ENGINE && NODE_LT_443) return done(); - var srvOpts = { - key: fs.readFileSync('test/fixtures/server.key'), - cert: fs.readFileSync('test/fixtures/server.crt'), - ca: fs.readFileSync('test/fixtures/ca.crt'), + 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 }; - var 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 opts = { + key: fs.readFileSync("test/fixtures/client.key"), + cert: fs.readFileSync("test/fixtures/client.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["polling"] }; - var srv = https.createServer(srvOpts, function (req, res) { + const srv = https.createServer(srvOpts, (req, res) => { res.writeHead(200); - res.end('hello world\n'); + res.end("hello world\n"); }); - var engine = eio({ transports: ['polling'], allowUpgrades: false }); + const engine = new Server({ + transports: ["polling"], + allowUpgrades: false, + allowEIO3: true + }); engine.attach(srv); - srv.listen(function () { - var port = srv.address().port; - var socket = new eioc.Socket('https://localhost:%d'.s(port), opts); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - expect(msg).to.be('hello'); + engine.on("connection", conn => { + conn.on("message", msg => { + expect(msg).to.be("hello"); done(); }); }); - socket.on('open', function () { - socket.send('hello'); + socket.on("open", () => { + socket.send("hello"); }); }); }); - it('should send and receive data with ca when not requiring auth (polling)', function (done) { - if (UWS_ENGINE && NODE_LT_443) return done(); - var srvOpts = { - key: fs.readFileSync('test/fixtures/server.key'), - cert: fs.readFileSync('test/fixtures/server.crt'), - ca: fs.readFileSync('test/fixtures/ca.crt'), + 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 }; - var opts = { - ca: fs.readFileSync('test/fixtures/ca.crt'), - transports: ['polling'] + const opts = { + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["polling"] }; - var srv = https.createServer(srvOpts, function (req, res) { + const srv = https.createServer(srvOpts, (req, res) => { res.writeHead(200); - res.end('hello world\n'); + res.end("hello world\n"); }); - var engine = eio({ transports: ['polling'], allowUpgrades: false }); + const engine = new Server({ + transports: ["polling"], + allowUpgrades: false, + allowEIO3: true + }); engine.attach(srv); - srv.listen(function () { - var port = srv.address().port; - var socket = new eioc.Socket('https://localhost:%d'.s(port), opts); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - expect(msg).to.be('hello'); + engine.on("connection", conn => { + conn.on("message", msg => { + expect(msg).to.be("hello"); done(); }); }); - socket.on('open', function () { - socket.send('hello'); + socket.on("open", () => { + socket.send("hello"); }); }); }); - it('should send and receive data with key and cert (ws)', function (done) { - var srvOpts = { - key: fs.readFileSync('test/fixtures/server.key'), - cert: fs.readFileSync('test/fixtures/server.crt'), - ca: fs.readFileSync('test/fixtures/ca.crt'), + 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 }; - var 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 opts = { + key: fs.readFileSync("test/fixtures/client.key"), + cert: fs.readFileSync("test/fixtures/client.crt"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["websocket"] }; - var srv = https.createServer(srvOpts, function (req, res) { + const srv = https.createServer(srvOpts, (req, res) => { res.writeHead(200); - res.end('hello world\n'); + res.end("hello world\n"); }); - var engine = eio({ transports: ['websocket'], allowUpgrades: false }); + const engine = new Server({ + transports: ["websocket"], + allowUpgrades: false, + allowEIO3: true + }); engine.attach(srv); - srv.listen(function () { - var port = srv.address().port; - var socket = new eioc.Socket('https://localhost:%d'.s(port), opts); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - expect(msg).to.be('hello'); + engine.on("connection", conn => { + conn.on("message", msg => { + expect(msg).to.be("hello"); done(); }); }); - socket.on('open', function () { - socket.send('hello'); + socket.on("open", () => { + socket.send("hello"); }); }); }); - it('should send and receive data with pfx (polling)', function (done) { - if (UWS_ENGINE && NODE_LT_443) return done(); - var srvOpts = { - key: fs.readFileSync('test/fixtures/server.key'), - cert: fs.readFileSync('test/fixtures/server.crt'), - ca: fs.readFileSync('test/fixtures/ca.crt'), + 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 }; - var opts = { - pfx: fs.readFileSync('test/fixtures/client.pfx'), - ca: fs.readFileSync('test/fixtures/ca.crt'), - transports: ['polling'] + const opts = { + pfx: fs.readFileSync("test/fixtures/client.pfx"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["polling"] }; - var srv = https.createServer(srvOpts, function (req, res) { + const srv = https.createServer(srvOpts, (req, res) => { res.writeHead(200); - res.end('hello world\n'); + res.end("hello world\n"); }); - var engine = eio({ transports: ['polling'], allowUpgrades: false }); + const engine = new Server({ + transports: ["polling"], + allowUpgrades: false, + allowEIO3: true + }); engine.attach(srv); - srv.listen(function () { - var port = srv.address().port; - var socket = new eioc.Socket('https://localhost:%d'.s(port), opts); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - expect(msg).to.be('hello'); + engine.on("connection", conn => { + conn.on("message", msg => { + expect(msg).to.be("hello"); done(); }); }); - socket.on('open', function () { - socket.send('hello'); + socket.on("open", () => { + socket.send("hello"); }); }); }); - it('should send and receive data with pfx (ws)', function (done) { - if (UWS_ENGINE && NODE_LT_443) return done(); - var srvOpts = { - key: fs.readFileSync('test/fixtures/server.key'), - cert: fs.readFileSync('test/fixtures/server.crt'), - ca: fs.readFileSync('test/fixtures/ca.crt'), + 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 }; - var opts = { - pfx: fs.readFileSync('test/fixtures/client.pfx'), - ca: fs.readFileSync('test/fixtures/ca.crt'), - transports: ['websocket'] + const opts = { + pfx: fs.readFileSync("test/fixtures/client.pfx"), + ca: fs.readFileSync("test/fixtures/ca.crt"), + transports: ["websocket"] }; - var srv = https.createServer(srvOpts, function (req, res) { + const srv = https.createServer(srvOpts, (req, res) => { res.writeHead(200); - res.end('hello world\n'); + res.end("hello world\n"); }); - var engine = eio({ transports: ['websocket'], allowUpgrades: false }); + const engine = new Server({ + transports: ["websocket"], + allowUpgrades: false, + allowEIO3: true + }); engine.attach(srv); - srv.listen(function () { - var port = srv.address().port; - var socket = new eioc.Socket('https://localhost:%d'.s(port), opts); + srv.listen(() => { + const port = srv.address().port; + const socket = new ClientSocket(`https://localhost:${port}`, opts); - engine.on('connection', function (conn) { - conn.on('message', function (msg) { - expect(msg).to.be('hello'); + engine.on("connection", conn => { + conn.on("message", msg => { + expect(msg).to.be("hello"); done(); }); }); - socket.on('open', function () { - socket.send('hello'); + socket.on("open", () => { + socket.send("hello"); }); }); }); }); - describe('send', function () { - describe('writeBuffer', function () { - it('should not empty until `drain` event (polling)', function (done) { - listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - var totalEvents = 2; - socket.on('open', function () { - socket.send('a'); - socket.send('b'); + 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', function () { + socket.transport.on("drain", () => { expect(socket.writeBuffer.length).to.eql(--totalEvents); totalEvents || done(); }); }); }); - it('should not empty until `drain` event (websocket)', function (done) { - listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var totalEvents = 2; - socket.on('open', function () { - socket.send('a'); - socket.send('b'); + 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', function () { + socket.transport.on("drain", () => { expect(socket.writeBuffer.length).to.eql(--totalEvents); totalEvents || done(); }); @@ -1820,21 +2431,23 @@ describe('server', function () { }); }); - describe('callback', function () { - it('should execute in order when message sent (client) (polling)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - var i = 0; - var j = 0; + 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', function (conn) { - conn.on('message', function (msg) { + engine.on("connection", conn => { + conn.on("message", msg => { conn.send(msg); }); }); - socket.on('open', function () { - socket.on('message', function (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); @@ -1844,10 +2457,13 @@ describe('server', function () { } }); - function sendFn () { - socket.send(j, (function (value) { - j++; - })(j)); + function sendFn() { + socket.send( + j, + (value => { + j++; + })(j) + ); } sendFn(); @@ -1855,20 +2471,22 @@ describe('server', function () { }); }); - it('should execute in order when message sent (client) (websocket)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var i = 0; - var j = 0; + 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', function (conn) { - conn.on('message', function (msg) { + engine.on("connection", conn => { + conn.on("message", msg => { conn.send(msg); }); }); - socket.on('open', function () { - socket.on('message', function (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); @@ -1878,10 +2496,13 @@ describe('server', function () { } }); - function sendFn () { - socket.send(j, (function (value) { - j++; - })(j)); + function sendFn() { + socket.send( + j, + (value => { + j++; + })(j) + ); } sendFn(); @@ -1889,25 +2510,27 @@ describe('server', function () { }); }); - it('should execute in order with payloads (client) (polling)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - var i = 0; - var lastCbFired = 0; + 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', function (conn) { - conn.on('message', function (msg) { + engine.on("connection", conn => { + conn.on("message", msg => { conn.send(msg); }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { expect(msg).to.eql(i + 1); i++; }); - function cb (value) { + function cb(value) { expect(value).to.eql(lastCbFired + 1); lastCbFired = value; if (value === 3) { @@ -1916,35 +2539,43 @@ describe('server', function () { } // 2 and 3 will be in the same payload - socket.once('flush', function () { - socket.send(2, function () { cb(2); }); - socket.send(3, function () { cb(3); }); + socket.once("flush", () => { + socket.send(2, () => { + cb(2); + }); + socket.send(3, () => { + cb(3); + }); }); - socket.send(1, function () { cb(1); }); + socket.send(1, () => { + cb(1); + }); }); }); }); - it('should execute in order with payloads (client) (websocket)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var i = 0; - var lastCbFired = 0; + 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', function (conn) { - conn.on('message', function (msg) { + engine.on("connection", conn => { + conn.on("message", msg => { conn.send(msg); }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { expect(msg).to.eql(i + 1); i++; }); - function cb (value) { + function cb(value) { expect(value).to.eql(lastCbFired + 1); lastCbFired = value; if (value === 3) { @@ -1953,84 +2584,94 @@ describe('server', function () { } // 2 and 3 will be in the same payload - socket.once('flush', function () { - socket.send(2, function () { cb(2); }); - socket.send(3, function () { cb(3); }); + socket.once("flush", () => { + socket.send(2, () => { + cb(2); + }); + socket.send(3, () => { + cb(3); + }); }); - socket.send(1, function () { cb(1); }); + socket.send(1, () => { + cb(1); + }); }); }); }); - it('should execute when message sent (polling)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - var i = 0; - var j = 0; + 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', function (conn) { - conn.send('a', function (transport) { + engine.on("connection", conn => { + conn.send("a", transport => { i++; }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { j++; }); }); - setTimeout(function () { + setTimeout(() => { expect(i).to.be(j); done(); }, 100); }); }); - it('should execute when message sent (websocket)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - var i = 0; - var j = 0; + 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', function (conn) { - conn.send('a', function (transport) { + engine.on("connection", conn => { + conn.send("a", transport => { i++; }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { j++; }); }); - setTimeout(function () { + setTimeout(() => { expect(i).to.be(j); done(); }, 100); }); }); - it('should execute once for each send', function (done) { - var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var a = 0; - var b = 0; - var c = 0; - var all = 0; + 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', function (conn) { - conn.send('a'); - conn.send('b'); - conn.send('c'); + engine.on("connection", conn => { + conn.send("a"); + conn.send("b"); + conn.send("c"); }); - socket.on('open', function () { - socket.on('message', function (msg) { - if (msg === 'a') a++; - if (msg === 'b') b++; - if (msg === 'c') 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); @@ -2043,89 +2684,93 @@ describe('server', function () { }); }); - it('should execute in multipart packet', function (done) { - var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - var i = 0; - var j = 0; + it("should execute in multipart packet", done => { + const engine = listen(port => { + const socket = new ClientSocket(`ws://localhost:${port}`); + let i = 0; + let j = 0; - engine.on('connection', function (conn) { - conn.send('b', function (transport) { + engine.on("connection", conn => { + conn.send("b", transport => { i++; }); - conn.send('a', function (transport) { + conn.send("a", transport => { i++; }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { j++; }); }); - setTimeout(function () { + setTimeout(() => { expect(i).to.be(j); done(); }, 200); }); }); - it('should execute in multipart packet (polling)', function (done) { - var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); - var i = 0; - var j = 0; + 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', function (conn) { - conn.send('d', function (transport) { + engine.on("connection", conn => { + conn.send("d", transport => { i++; }); - conn.send('c', function (transport) { + conn.send("c", transport => { i++; }); - conn.send('b', function (transport) { + conn.send("b", transport => { i++; }); - conn.send('a', function (transport) { + conn.send("a", transport => { i++; }); }); - socket.on('open', function () { - socket.on('message', function (msg) { + socket.on("open", () => { + socket.on("message", msg => { j++; }); }); - setTimeout(function () { + setTimeout(() => { expect(i).to.be(j); done(); }, 200); }); }); - it('should clean callback references when socket gets closed with pending callbacks', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); + 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', function (conn) { - socket.transport.on('pollComplete', function () { - conn.send('a', function (transport) { - done(new Error('Test invalidation')); + 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')); + 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', function (reason) { + conn.on("close", reason => { expect(conn.packetsFn).to.be.empty(); expect(conn.sentCallbackFn).to.be.empty(); done(); @@ -2134,53 +2779,80 @@ describe('server', function () { }); }); - it('should not execute when it is not actually sent (polling)', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { transports: ['polling'] }); + 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', function (msg) { + socket.transport.on("pollComplete", msg => { socket.close(); }); - engine.on('connection', function (conn) { - var err; - conn.send('a'); - conn.send('b', function (transport) { - err = new Error('Test invalidation'); + engine.on("connection", conn => { + let err; + conn.send("a"); + conn.send("b", transport => { + err = new Error("Test invalidation"); }); - conn.on('close', function (reason) { + conn.on("close", reason => { done(err); }); }); }); }); }); + + describe("pre-encoded content", () => { + it("should use the pre-encoded content", done => { + engine = listen(port => { + client = new ClientSocket(`ws://localhost:${port}`, { + transports: ["websocket"] + }); + + engine.on("connection", conn => { + conn.send("test", { + wsPreEncoded: "4test pre-encoded" + }); + }); + + client.on("message", msg => { + expect(msg).to.be("test pre-encoded"); + done(); + }); + }); + }); + }); }); - describe('packet', function () { - it('should emit when socket receives packet', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('packet', function (packet) { - expect(packet.type).to.be('message'); - expect(packet.data).to.be('a'); + 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', function () { - socket.send('a'); + socket.on("open", () => { + socket.send("a"); }); }); }); - it('should emit when receives ping', function (done) { - var engine = listen({ allowUpgrades: false, pingInterval: 4 }, function (port) { - eioc('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('packet', function (packet) { + 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(); - expect(packet.type).to.be('ping'); + if (process.env.EIO_CLIENT === "3") { + expect(packet.type).to.be("ping"); + } else { + expect(packet.type).to.be("pong"); + } done(); }); }); @@ -2188,28 +2860,32 @@ describe('server', function () { }); }); - describe('packetCreate', function () { - it('should emit before socket send message', function (done) { - var engine = listen({ allowUpgrades: false }, function (port) { - eioc('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('packetCreate', function (packet) { - expect(packet.type).to.be('message'); - expect(packet.data).to.be('a'); + 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'); + conn.send("a"); }); }); }); - it('should emit before send pong', function (done) { - var engine = listen({ allowUpgrades: false, pingInterval: 4 }, function (port) { - eioc('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.on('packetCreate', function (packet) { + 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(); - expect(packet.type).to.be('pong'); + if (process.env.EIO_CLIENT === "3") { + expect(packet.type).to.be("pong"); + } else { + expect(packet.type).to.be("ping"); + } done(); }); }); @@ -2217,24 +2893,25 @@ describe('server', function () { }); }); - describe('upgrade', function () { - it('should upgrade', function (done) { - var engine = listen(function (port) { + describe("upgrade", () => { + it("should upgrade", done => { + const engine = listen(port => { // it takes both to send 50 to verify - var ready = 2; - var closed = 2; - function finish () { - setTimeout(function () { + let ready = 2; + let closed = 2; + + function finish() { + setTimeout(() => { socket.close(); }, 10); } // server - engine.on('connection', function (conn) { - var lastSent = 0; - var lastReceived = 0; - var upgraded = false; - var interval = setInterval(function () { + engine.on("connection", conn => { + let lastSent = 0; + let lastReceived = 0; + let upgraded = false; + const interval = setInterval(() => { lastSent++; conn.send(lastSent); if (50 === lastSent) { @@ -2243,23 +2920,23 @@ describe('server', function () { } }, 2); - expect(conn.request._query.transport).to.be('polling'); + expect(conn.request._query.transport).to.be("polling"); - conn.on('message', function (msg) { - expect(conn.request._query).to.be.an('object'); + conn.on("message", msg => { + expect(conn.request._query).to.be.an("object"); lastReceived++; expect(msg).to.eql(lastReceived); }); - conn.on('upgrade', function (to) { - expect(conn.request._query.transport).to.be('polling'); + 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'); + expect(to.name).to.be("websocket"); + expect(conn.transport.name).to.be("websocket"); }); - conn.on('close', function (reason) { - expect(reason).to.be('transport close'); + 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); @@ -2268,12 +2945,12 @@ describe('server', function () { }); // client - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - socket.on('open', function () { - var lastSent = 0; - var lastReceived = 0; - var upgrades = 0; - var interval = setInterval(function () { + 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) { @@ -2281,9 +2958,9 @@ describe('server', function () { --ready || finish(); } }, 2); - socket.on('upgrading', function (to) { + 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'); + expect(to.name).to.equal("websocket"); upgrades++; // force send a few packets to ensure we test buffer transfer @@ -2294,16 +2971,16 @@ describe('server', function () { expect(socket.writeBuffer).to.not.be.empty(); }); - socket.on('upgrade', function (to) { - expect(to.name).to.equal('websocket'); + socket.on("upgrade", to => { + expect(to.name).to.equal("websocket"); upgrades++; }); - socket.on('message', function (msg) { + socket.on("message", msg => { lastReceived++; expect(lastReceived).to.eql(msg); }); - socket.on('close', function (reason) { - expect(reason).to.be('forced close'); + socket.on("close", reason => { + expect(reason).to.be("forced close"); expect(lastSent).to.be(50); expect(upgrades).to.be(2); --closed || done(); @@ -2311,380 +2988,712 @@ describe('server', function () { }); }); - // attach another engine to make sure it doesn't break upgrades - eio.attach(engine.httpServer, { path: '/foo' }); + if (engine.httpServer) { + // attach another engine to make sure it doesn't break upgrades + attach(engine.httpServer, { path: "/foo" }); + } }); }); - describe('http compression', function () { - function getSidFromResponse (res) { - var c = cookieMod.parse(res.headers['set-cookie'][0]); + 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', function (done) { - var engine = listen({ transports: ['polling'] }, function (port) { - engine.on('connection', function (conn) { - var buf = Buffer.allocUnsafe(1024); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; + 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/default/?transport=polling' - }, function (res) { - var sid = getSidFromResponse(res); - http.get({ + http.get( + { port: port, - path: '/engine.io/default/?transport=polling&sid=' + sid, - headers: { 'Accept-Encoding': 'gzip, deflate' } - }, function (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', function (done) { - var engine = listen({ transports: ['polling'] }, function (port) { - engine.on('connection', function (conn) { - var buf = Buffer.allocUnsafe(1024); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; - conn.send(buf); - }); - - http.get({ - port: port, - path: '/engine.io/default/?transport=polling' - }, function (res) { - var sid = getSidFromResponse(res); - http.get({ - port: port, - path: '/engine.io/default/?transport=polling&sid=' + sid, - headers: { 'Accept-Encoding': 'deflate' } - }, function (res) { - expect(res.headers['content-encoding']).to.equal('deflate'); - res.pipe(zlib.createDeflate()) - .on('error', done) - .on('end', done) - .resume(); - }); - }); - }); - }); - - it('should set threshold', function (done) { - var engine = listen({ transports: ['polling'], httpCompression: { threshold: 0 } }, function (port) { - engine.on('connection', function (conn) { - var buf = Buffer.allocUnsafe(10); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; - conn.send(buf); - }); - - http.get({ - port: port, - path: '/engine.io/default/?transport=polling' - }, function (res) { - var sid = getSidFromResponse(res); - http.get({ - port: port, - path: '/engine.io/default/?transport=polling&sid=' + sid, - headers: { 'Accept-Encoding': 'gzip, deflate' } - }, function (res) { - expect(res.headers['content-encoding']).to.equal('gzip'); - done(); - }); - }); + 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 disable compression', function (done) { - var engine = listen({ transports: ['polling'], httpCompression: false }, function (port) { - engine.on('connection', function (conn) { - var buf = Buffer.allocUnsafe(1024); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; + 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/default/?transport=polling' - }, function (res) { - var sid = getSidFromResponse(res); - http.get({ + http.get( + { port: port, - path: '/engine.io/default/?transport=polling&sid=' + sid, - headers: { 'Accept-Encoding': 'gzip, deflate' } - }, function (res) { - expect(res.headers['content-encoding']).to.be(undefined); - done(); - }); - }); - }); + 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', function (done) { - var engine = listen({ transports: ['polling'] }, function (port) { - engine.on('connection', function (conn) { - var buf = Buffer.allocUnsafe(1024); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; + 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/default/?transport=polling' - }, function (res) { - var sid = getSidFromResponse(res); - http.get({ + http.get( + { port: port, - path: '/engine.io/default/?transport=polling&sid=' + sid, - headers: { 'Accept-Encoding': 'gzip, deflate' } - }, function (res) { - expect(res.headers['content-encoding']).to.be(undefined); - done(); - }); - }); + 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', function (done) { - var engine = listen({ transports: ['polling'] }, function (port) { - engine.on('connection', function (conn) { - var buf = Buffer.allocUnsafe(100); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; + 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/default/?transport=polling' - }, function (res) { - var sid = getSidFromResponse(res); - http.get({ + http.get( + { port: port, - path: '/engine.io/default/?transport=polling&sid=' + sid, - headers: { 'Accept-Encoding': 'gzip, deflate' } - }, function (res) { - expect(res.headers['content-encoding']).to.be(undefined); - done(); - }); - }); + 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('permessage-deflate', function () { - it('should set threshold', function (done) { - var engine = listen({ transports: ['websocket'], perMessageDeflate: { threshold: 0 } }, function (port) { - engine.on('connection', function (conn) { - var socket = conn.transport.socket; - var send = socket.send; - socket.send = function (data, opts, callback) { - socket.send = send; - socket.send(data, opts, callback); - - expect(opts.compress).to.be(true); - conn.close(); - done(); - }; + describe("permessage-deflate", () => { + it("should set threshold", function(done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const engine = listen( + { transports: ["websocket"], perMessageDeflate: { threshold: 0 } }, + port => { + engine.on("connection", conn => { + const socket = conn.transport.socket; + const send = socket.send; + socket.send = (data, opts, callback) => { + socket.send = send; + socket.send(data, opts, callback); + + expect(opts.compress).to.be(true); + conn.close(); + done(); + }; - var buf = Buffer.allocUnsafe(100); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; - conn.send(buf, { compress: true }); - }); - eioc('http://localhost:%d'.s(port), { transports: ['websocket'] }); - }); + const buf = Buffer.allocUnsafe(100); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf, { compress: true }); + }); + new ClientSocket(`http://localhost:${port}`, { + transports: ["websocket"] + }); + } + ); }); - it('should not compress when the byte size is below threshold', function (done) { - var engine = listen({ transports: ['websocket'] }, function (port) { - engine.on('connection', function (conn) { - var socket = conn.transport.socket; - var send = socket.send; - socket.send = function (data, opts, callback) { - socket.send = send; - socket.send(data, opts, callback); - - expect(opts.compress).to.be(false); - conn.close(); - done(); - }; + it("should not compress when the byte size is below threshold", function(done) { + if (process.env.EIO_WS_ENGINE === "uws") { + return this.skip(); + } + const engine = listen( + { transports: ["websocket"], perMessageDeflate: true }, + port => { + engine.on("connection", conn => { + const socket = conn.transport.socket; + const send = socket.send; + socket.send = (data, opts, callback) => { + socket.send = send; + socket.send(data, opts, callback); + + expect(opts.compress).to.be(false); + conn.close(); + done(); + }; - var buf = Buffer.allocUnsafe(100); - for (var i = 0; i < buf.length; i++) buf[i] = i % 0xff; - conn.send(buf, { compress: true }); - }); - eioc('http://localhost:%d'.s(port), { transports: ['websocket'] }); - }); + const buf = Buffer.allocUnsafe(100); + for (let i = 0; i < buf.length; i++) buf[i] = i % 0xff; + conn.send(buf, { compress: true }); + }); + new ClientSocket(`http://localhost:${port}`, { + transports: ["websocket"] + }); + } + ); }); }); - describe('extraHeaders', function () { + describe("extraHeaders", function() { this.timeout(5000); - var 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' + 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) { - var engine = listen(function (port) { - var socket = new eioc.Socket('ws://localhost:%d'.s(port), { + function testForTransport(transport, done) { + const engine = listen(port => { + const socket = new ClientSocket(`ws://localhost:${port}`, { extraHeaders: headers, transports: [transport] }); - engine.on('connection', function (conn) { - for (var h in headers) { + engine.on("connection", conn => { + for (let h in headers) { expect(conn.request.headers[h]).to.equal(headers[h]); } done(); }); - socket.on('open', function () {}); + socket.on("open", () => {}); }); } - it('should arrive from client to server via WebSockets', function (done) { - testForTransport('websocket', done); + it("should arrive from client to server via WebSockets", done => { + testForTransport("websocket", done); }); - it('should arrive from client to server via XMLHttpRequest', function (done) { - testForTransport('polling', done); + it("should arrive from client to server via XMLHttpRequest", done => { + testForTransport("polling", done); }); }); - describe('response headers', function () { - function testForHeaders (headers, done) { - var engine = listen(function (port) { - engine.on('connection', function (conn) { - conn.transport.once('headers', function (headers) { - expect(headers['X-XSS-Protection']).to.be('0'); + describe("response headers", () => { + function testForHeaders(headers, done) { + const engine = listen(port => { + engine.on("connection", conn => { + conn.transport.once("headers", headers => { + expect(headers["X-XSS-Protection"]).to.be("0"); conn.close(); done(); }); - conn.send('hi'); + conn.send("hi"); }); - eioc('ws://localhost:%d'.s(port), { + new ClientSocket(`ws://localhost:${port}`, { extraHeaders: headers, - transports: ['polling'] + transports: ["polling"] }); }); } - it('should contain X-XSS-Protection: 0 for IE8', function (done) { - var 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)' }; + 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, done); }); - it('should contain X-XSS-Protection: 0 for IE11', function (done) { - var headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko' }; + 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, done); }); - }); - describe('cors', function () { - it('should handle OPTIONS requests', function (done) { - listen({handlePreflightRequest: true}, function (port) { - request.options('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'polling' }) - .end(function (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'); - expect(res.header['access-control-allow-credentials']).to.be('true'); - expect(res.header['access-control-allow-origin']).to.be('http://engine.io'); - 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 not handle OPTIONS requests', function (done) { - listen({handlePreflightRequest: false}, function (port) { - request.options('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'polling' }) - .end(function (err, res) { - expect(err).to.be.an(Error); - expect(res.status).to.be(501); - expect(res.body.code).to.be(undefined); - done(); + 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 handle OPTIONS requests with the given function', function (done) { - var handlePreflightRequest = function (req, res) { - var headers = {}; - if (req.headers.origin) { - headers['Access-Control-Allow-Credentials'] = 'true'; - headers['Access-Control-Allow-Origin'] = req.headers.origin; - } else { - headers['Access-Control-Allow-Origin'] = '*'; + 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(); + }); } - headers['Access-Control-Allow-Methods'] = 'GET,HEAD,PUT,PATCH,POST,DELETE'; - headers['Access-Control-Allow-Headers'] = 'origin, content-type, accept'; - res.writeHead(200, headers); - res.end(); - }; - listen({handlePreflightRequest: handlePreflightRequest}, function (port) { - request.options('http://localhost:%d/engine.io/default/'.s(port)) - .set('Origin', 'http://engine.io') - .query({ transport: 'polling' }) - .end(function (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-credentials']).to.be('true'); - 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('origin, content-type, accept'); + ); + }); + + 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', function () { - it('should allow loading of other websocket server implementation like uws', function (done) { - var engine = listen({ allowUpgrades: false, wsEngine: 'uws' }, function (port) { - expect(engine.ws instanceof require('uws').Server).to.be.ok(); - var socket = new eioc.Socket('ws://localhost:%d'.s(port)); - engine.on('connection', function (conn) { - conn.send('a'); - }); - socket.on('open', function () { - socket.on('message', function (msg) { - expect(msg).to.be('a'); - 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', function () { - it('should be defined (polling)', function (done) { - var engine = listen({ transports: ['polling'] }, port => { - eioc('ws://localhost:%d'.s(port), { transports: ['polling'] }); - engine.on('connection', socket => { - expect(socket.remoteAddress).to.be('::ffff:127.0.0.1'); + 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)', function (done) { - var engine = listen({ transports: ['websocket'] }, port => { - eioc('ws://localhost:%d'.s(port), { transports: ['websocket'] }); - engine.on('connection', socket => { - expect(socket.remoteAddress).to.be('::ffff:127.0.0.1'); + 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/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..72ed6e2d5 --- /dev/null +++ b/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/wrapper.mjs b/wrapper.mjs new file mode 100644 index 000000000..6b0005b7c --- /dev/null +++ b/wrapper.mjs @@ -0,0 +1,3 @@ +import lib from "./build/engine.io.js"; + +export const { Server, Socket, Transport, transports, listen, attach, parser, protocol } = lib;