Merge remote-tracking branch 'engine.io-client/main' into monorepo

Source: https://github.com/socketio/engine.io-client
This commit is contained in:
Damien Arrachequesne
2024-07-08 11:00:19 +02:00
68 changed files with 35756 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
test/support/public/engine.io.min.js
lib/contrib/*

View File

@@ -0,0 +1,686 @@
# History
| Version | Release date | Bundle size (UMD min+gzip) |
|-------------------------------------------------------------------------------------------------------------|----------------|----------------------------|
| [6.6.0](#660-2024-06-21) | June 2024 | `8.6 KB` |
| [6.5.4](#654-2024-06-18) (from the [6.5.x](https://github.com/socketio/engine.io-client/tree/6.5.x) branch) | June 2024 | `8.8 KB` |
| [3.5.4](#354-2024-06-18) (from the [3.5.x](https://github.com/socketio/engine.io-client/tree/3.5.x) branch) | June 2024 | `-` |
| [6.5.3](#653-2023-11-09) | November 2023 | `8.8 KB` |
| [6.5.2](#652-2023-08-01) | August 2023 | `8.8 KB` |
| [6.5.1](#651-2023-06-28) | June 2023 | `8.4 KB` |
| [6.5.0](#650-2023-06-16) | June 2023 | `7.8 KB` |
| [6.4.0](#640-2023-02-06) | February 2023 | `7.8 KB` |
| [6.3.1](#631-2023-02-04) | February 2023 | `7.8 KB` |
| [6.3.0](#630-2023-01-10) | January 2023 | `8.0 KB` |
| [6.2.3](#623-2022-10-13) | October 2022 | `7.8 KB` |
| [3.5.3](#353-2022-09-07) | September 2022 | `-` |
| [6.2.2](#622-2022-05-02) | May 2022 | `7.8 KB` |
| [6.2.1](#621-2022-04-17) | April 2022 | `7.8 KB` |
| [6.2.0](#620-2022-04-17) | April 2022 | `7.8 KB` |
| [6.0.3](#603-2021-11-14) (from the [6.0.x](https://github.com/socketio/engine.io-client/tree/6.0.x) branch) | November 2021 | `7.4 KB` |
| [6.1.1](#611-2021-11-14) | November 2021 | `7.4 KB` |
| [6.1.0](#610-2021-11-08) | November 2021 | `7.4 KB` |
| [6.0.2](#602-2021-10-15) | October 2021 | `7.4 KB` |
| [6.0.1](#601-2021-10-14) | October 2021 | `7.4 KB` |
| [**6.0.0**](#600-2021-10-08) | October 2021 | `7.5 KB` |
| [5.2.0](#520-2021-08-29) | August 2021 | `9.4 KB` |
| [5.1.2](#512-2021-06-24) | June 2021 | `9.3 KB` |
| [5.1.1](#511-2021-05-11) | May 2021 | `9.2 KB` |
| [4.1.4](#414-2021-05-05) (from the [4.1.x](https://github.com/socketio/engine.io-client/tree/4.1.x) branch) | May 2021 | `9.1 KB` |
| [3.5.2](#352-2021-05-05) (from the [3.5.x](https://github.com/socketio/engine.io-client/tree/3.5.x) branch) | May 2021 | `-` |
| [5.1.0](#510-2021-05-04) | May 2021 | `9.2 KB` |
| [5.0.1](#501-2021-03-31) | March 2021 | `9.2 KB` |
| [**5.0.0**](#500-2021-03-10) | March 2021 | `9.3 KB` |
| [3.5.1](#351-2021-03-02) (from the [3.5.x](https://github.com/socketio/engine.io-client/tree/3.5.x) branch) | March 2021 | `-` |
| [4.1.2](#412-2021-02-25) | February 2021 | `9.2 KB` |
| [4.1.1](#411-2021-02-02) | February 2021 | `9.1 KB` |
| [4.1.0](#410-2021-01-14) | January 2021 | `9.1 KB` |
# Release notes
## [6.6.0](https://github.com/socketio/engine.io-client/compare/6.5.3...6.6.0) (2024-06-21)
### Features
#### Custom transport implementations
The `transports` option now accepts an array of transport implementations:
```js
import { Socket, XHR, WebSocket } from "engine.io-client";
const socket = new Socket({
transports: [XHR, WebSocket]
});
```
Here is the list of provided implementations:
| Transport | Description |
|-----------------|------------------------------------------------------------------------------------------------------|
| `Fetch` | HTTP long-polling based on the built-in `fetch()` method. |
| `NodeXHR` | HTTP long-polling based on the `XMLHttpRequest` object provided by the `xmlhttprequest-ssl` package. |
| `XHR` | HTTP long-polling based on the built-in `XMLHttpRequest` object. |
| `NodeWebSocket` | WebSocket transport based on the `WebSocket` object provided by the `ws` package. |
| `WebSocket` | WebSocket transport based on the built-in `WebSocket` object. |
| `WebTransport` | WebTransport transport based on the built-in `WebTransport` object. |
Usage:
| Transport | browser | Node.js | Deno | Bun |
|-----------------|--------------------|------------------------|--------------------|--------------------|
| `Fetch` | :white_check_mark: | :white_check_mark: (1) | :white_check_mark: | :white_check_mark: |
| `NodeXHR` | | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| `XHR` | :white_check_mark: | | | |
| `NodeWebSocket` | | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| `WebSocket` | :white_check_mark: | :white_check_mark: (2) | :white_check_mark: | :white_check_mark: |
| `WebTransport` | :white_check_mark: | :white_check_mark: | | |
(1) since [v18.0.0](https://nodejs.org/api/globals.html#fetch)
(2) since [v21.0.0](https://nodejs.org/api/globals.html#websocket)
Added in [f4d898e](https://github.com/socketio/engine.io-client/commit/f4d898ee9652939a4550a41ac0e8143056154c0a) and [b11763b](https://github.com/socketio/engine.io-client/commit/b11763beecfe4622867b4dec9d1db77460733ffb).
#### Transport tree-shaking
The feature above also comes with the ability to exclude the code related to unused transports (a.k.a. "tree-shaking"):
```js
import { SocketWithoutUpgrade, WebSocket } from "engine.io-client";
const socket = new SocketWithoutUpgrade({
transports: [WebSocket]
});
```
In that case, the code related to HTTP long-polling and WebTransport will be excluded from the final bundle.
Added in [f4d898e](https://github.com/socketio/engine.io-client/commit/f4d898ee9652939a4550a41ac0e8143056154c0a)
#### Test each low-level transports
When setting the `tryAllTransports` option to `true`, if the first transport (usually, HTTP long-polling) fails, then the other transports will be tested too:
```js
import { Socket } from "engine.io-client";
const socket = new Socket({
tryAllTransports: true
});
```
This feature is useful in two cases:
- when HTTP long-polling is disabled on the server, or if CORS fails
- when WebSocket is tested first (with `transports: ["websocket", "polling"]`)
The only potential downside is that the connection attempt could take more time in case of failure, as there have been reports of WebSocket connection errors taking several seconds before being detected (that's one reason for using HTTP long-polling first). That's why the option defaults to `false` for now.
Added in [579b243](https://github.com/socketio/engine.io-client/commit/579b243e89ac7dc58233f9844ef70817364ecf52).
### Bug Fixes
* add some randomness to the cache busting string generator ([b624c50](https://github.com/socketio/engine.io-client/commit/b624c508325615fe5f0ba82293d14831d8861324))
* fix cookie management with WebSocket (Node.js only) ([e105551](https://github.com/socketio/engine.io-client/commit/e105551ef17ff8a23aa3ebdea9119619ae4208ad))
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) (no change)
## [6.5.4](https://github.com/socketio/engine.io-client/compare/6.5.3...6.5.4) (2024-06-18)
This release contains a bump of the `ws` dependency, which includes an important [security fix](https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c).
Advisory: https://github.com/advisories/GHSA-3h5v-q93c-6h6q
### Dependencies
- [`ws@~8.17.1`](https://github.com/websockets/ws/releases/tag/8.17.1) ([diff](https://github.com/websockets/ws/compare/8.11.0...8.17.1))
## [3.5.4](https://github.com/socketio/engine.io-client/compare/3.5.3...3.5.4) (2024-06-18)
This release contains a bump of the `ws` dependency, which includes an important [security fix](https://github.com/websockets/ws/commit/e55e5106f10fcbaac37cfa89759e4cc0d073a52c).
Advisory: https://github.com/advisories/GHSA-3h5v-q93c-6h6q
### Dependencies
- [`ws@~7.5.10`](https://github.com/websockets/ws/releases/tag/7.5.10) ([diff](https://github.com/websockets/ws/compare/7.4.2...7.5.10))
## [6.5.3](https://github.com/socketio/engine.io-client/compare/6.5.2...6.5.3) (2023-11-09)
### Bug Fixes
* add a maximum length for the URL ([707597d](https://github.com/socketio/engine.io-client/commit/707597df26abfa1e6b569b2a62918dfcc8b80b5d))
* improve compatibility with node16 module resolution ([#711](https://github.com/socketio/engine.io-client/issues/711)) ([46ef851](https://github.com/socketio/engine.io-client/commit/46ef8512edac758069ed4d519f7517bafbace4a9))
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [6.5.2](https://github.com/socketio/engine.io-client/compare/6.5.1...6.5.2) (2023-08-01)
### Bug Fixes
* **webtransport:** add proper framing ([d55c39e](https://github.com/socketio/engine.io-client/commit/d55c39e0ed5cb7b3a34875a398efc111c91184f6))
* **webtransport:** honor the binaryType attribute ([8270e00](https://github.com/socketio/engine.io-client/commit/8270e00d5b865278d136a4d349b344cbc2b38dc5))
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [6.5.1](https://github.com/socketio/engine.io-client/compare/6.5.0...6.5.1) (2023-06-28)
### Bug Fixes
* make closeOnBeforeunload default to false ([a63066b](https://github.com/socketio/engine.io-client/commit/a63066bdc8ae9e6746c3113d06c2ead78f4a4851))
* **webtransport:** properly handle abruptly closed connections ([cf6aa1f](https://github.com/socketio/engine.io-client/commit/cf6aa1f43c27a56c076bf26fddfce74bfeb65040))
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [6.5.0](https://github.com/socketio/engine.io-client/compare/6.4.0...6.5.0) (2023-06-16)
### Features
#### Support for WebTransport
The Engine.IO client can now use WebTransport as the underlying transport.
WebTransport is a web API that uses the HTTP/3 protocol as a bidirectional transport. It's intended for two-way communications between a web client and an HTTP/3 server.
References:
- https://w3c.github.io/webtransport/
- https://developer.mozilla.org/en-US/docs/Web/API/WebTransport
- https://developer.chrome.com/articles/webtransport/
**For Node.js clients**: until WebTransport support lands [in Node.js](https://github.com/nodejs/node/issues/38478), you can use the `@fails-components/webtransport` package:
```js
import { WebTransport } from "@fails-components/webtransport";
global.WebTransport = WebTransport;
```
Added in [7195c0f](https://github.com/socketio/engine.io-client/commit/7195c0f305b482f7b1ca2ed812030caaf72c0906).
#### Cookie management for the Node.js client
When setting the `withCredentials` option to `true`, the Node.js client will now include the cookies in the HTTP requests, making it easier to use it with cookie-based sticky sessions.
```js
import { Socket } from "engine.io-client";
const socket = new Socket("https://example.com", {
withCredentials: true
});
```
Added in [5fc88a6](https://github.com/socketio/engine.io-client/commit/5fc88a62d4017cdc144fa39b9755deadfff2db34).
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [6.4.0](https://github.com/socketio/engine.io-client/compare/6.3.1...6.4.0) (2023-02-06)
The minor bump is due to changes on the server side.
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [6.3.1](https://github.com/socketio/engine.io-client/compare/6.3.0...6.3.1) (2023-02-04)
### Bug Fixes
* **typings:** do not expose browser-specific types ([37d7a0a](https://github.com/socketio/engine.io-client/commit/37d7a0aa791a4666ca405b11d0d8bdb199222e50))
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [6.3.0](https://github.com/socketio/engine.io-client/compare/6.2.3...6.3.0) (2023-01-10)
### Bug Fixes
* properly parse relative URL with a "@" character ([12b7d78](https://github.com/socketio/engine.io-client/commit/12b7d7817e9c0016c970f903de15ed8b4255ea90))
* use explicit context for setTimeout function ([#699](https://github.com/socketio/engine.io-client/issues/699)) ([047f420](https://github.com/socketio/engine.io-client/commit/047f420b86a669752536ff425261e7be60a80692))
### Features
* add the "addTrailingSlash" option ([#694](https://github.com/socketio/engine.io-client/issues/694)) ([21a6e12](https://github.com/socketio/engine.io-client/commit/21a6e1219add92157c5442537d24fbe1129a50f5))
The trailing slash which was added by default can now be disabled:
```js
import { Socket } from "engine.io-client";
const socket = new Socket("https://example.com", {
addTrailingSlash: false
});
```
In the example above, the request URL will be `https://example.com/engine.io` instead of `https://example.com/engine.io/`.
### Dependencies
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) ([diff](https://github.com/websockets/ws/compare/8.2.3...8.11.0))
## [6.2.3](https://github.com/socketio/engine.io-client/compare/6.2.2...6.2.3) (2022-10-13)
### Bug Fixes
* properly clear "beforeunload" event listener ([99925a4](https://github.com/socketio/engine.io-client/commit/99925a47750f66d2ad36313243545181512579ee))
### Dependencies
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
## [3.5.3](https://github.com/socketio/engine.io-client/compare/3.5.2...3.5.3) (2022-09-07)
### Bug Fixes
* fix usage with vite ([280de36](https://github.com/socketio/engine.io-client/commit/280de368092b17648b59b7467fa49f2425edcd45))
### Dependencies
- [`ws@~7.4.2`](https://github.com/websockets/ws/releases/tag/7.4.2) (no change)
## [6.2.2](https://github.com/socketio/engine.io-client/compare/6.2.1...6.2.2) (2022-05-02)
### Bug Fixes
* simplify the check for WebSocket availability ([f158c8e](https://github.com/socketio/engine.io-client/commit/f158c8e255be9e849313e53201adf1642c60345a))
This check was added for the flashsocket transport, which has been deprecated for a while now ([1]). But it fails with latest webpack versions, as the expression `"__initialize" in WebSocket` gets evaluated to `true`.
* use named export for globalThis shim ([#688](https://github.com/socketio/engine.io-client/issues/688)) ([32878ea](https://github.com/socketio/engine.io-client/commit/32878ea047c38e2b2f0444e828ac71f4d833971f))
Default export of globalThis seems to have a problem in the "browser" field when the library is loaded asynchronously with webpack.
## [6.2.1](https://github.com/socketio/engine.io-client/compare/6.2.0...6.2.1) (2022-04-17)
# [6.2.0](https://github.com/socketio/engine.io-client/compare/6.1.1...6.2.0) (2022-04-17)
### Features
* add details to the "close" event ([b9252e2](https://github.com/socketio/engine.io-client/commit/b9252e207413a850db7e4f0f0ef7dd2ef0ed26da))
The close event will now include additional details to help debugging if anything has gone wrong.
Example when a payload is over the maxHttpBufferSize value in HTTP long-polling mode:
```js
socket.on("close", (reason, details) => {
console.log(reason); // "transport error"
// in that case, details is an error object
console.log(details.message); "xhr post error"
console.log(details.description); // 413 (the HTTP status of the response)
// details.context refers to the XMLHttpRequest object
console.log(details.context.status); // 413
console.log(details.context.responseText); // ""
});
```
Note: the error object was already included before this commit and is kept for backward compatibility.
* slice write buffer according to the maxPayload value ([46fdc2f](https://github.com/socketio/engine.io-client/commit/46fdc2f0ed352b454614247406689edc9d908927))
The server will now include a "maxPayload" field in the handshake details, allowing the clients to decide how many
packets they have to send to stay under the maxHttpBufferSize value.
## [6.0.3](https://github.com/socketio/engine.io-client/compare/6.0.2...6.0.3) (2021-11-14)
Some bug fixes were backported from master, to be included by the latest `socket.io-client` version.
### Bug Fixes
* add package name in nested package.json ([32511ee](https://github.com/socketio/engine.io-client/commit/32511ee32a0a6122e99db35833ed948aa4e427ac))
* fix vite build for CommonJS users ([9fcaf58](https://github.com/socketio/engine.io-client/commit/9fcaf58d18c013c0b92fdaf27481f0383efb3658))
## [6.1.1](https://github.com/socketio/engine.io-client/compare/6.1.0...6.1.1) (2021-11-14)
### Bug Fixes
* add package name in nested package.json ([6e798fb](https://github.com/socketio/engine.io-client/commit/6e798fbb5b11a1cfec03ece3dfce03213b5f9a12))
* fix vite build for CommonJS users ([c557707](https://github.com/socketio/engine.io-client/commit/c557707fb694bd10397b4cd8b4ec2fbe59128faa))
# [6.1.0](https://github.com/socketio/engine.io-client/compare/6.0.2...6.1.0) (2021-11-08)
The minor bump is due to changes on the server side.
### Bug Fixes
* **typings:** allow any value in the query option ([018e1af](https://github.com/socketio/engine.io-client/commit/018e1afcc5ef5eac81e9e1629db053bda44120ee))
* **typings:** allow port to be a number ([#680](https://github.com/socketio/engine.io-client/issues/680)) ([8f68f77](https://github.com/socketio/engine.io-client/commit/8f68f77825af069fe2c612a3200a025d4130ac0a))
## [6.0.2](https://github.com/socketio/engine.io-client/compare/6.0.1...6.0.2) (2021-10-15)
### Bug Fixes
* **bundle:** fix vite build ([faa9f31](https://github.com/socketio/engine.io-client/commit/faa9f318e70cd037af79bfa20e9d21b284ddf257))
## [6.0.1](https://github.com/socketio/engine.io-client/compare/6.0.0...6.0.1) (2021-10-14)
### Bug Fixes
* fix usage with vite ([4971914](https://github.com/socketio/engine.io-client/commit/49719142f65e23efa65fca4f66765ded5d955972))
# [6.0.0](https://github.com/socketio/engine.io-client/compare/5.2.0...6.0.0) (2021-10-08)
This major release contains three important changes:
- the codebase was migrated to TypeScript ([7245b80](https://github.com/socketio/engine.io-client/commit/7245b803e0c8d57cfc1f1cd8b8c8d598e8397967))
- rollup is now used instead of webpack to create the bundles ([27de300](https://github.com/socketio/engine.io-client/commit/27de300de42420ab59a02ec7a3445e636cbcc78e))
- code that provided support for ancient browsers (think IE8) was removed ([c656192](https://github.com/socketio/engine.io-client/commit/c6561928be628084fd2f5e7a70943c8e5c582873) and [b2c7381](https://github.com/socketio/engine.io-client/commit/b2c73812e978489b5dfbe516a26b6b8fd628856d))
There is now three distinct builds (in the build/ directory):
- CommonJS
- ESM with debug
- ESM without debug (rationale here: [00d7e7d](https://github.com/socketio/engine.io-client/commit/00d7e7d7ee85b4cfa6f9f547203cc692083ac61c))
And three bundles (in the dist/ directory) :
- `engine.io.js`: unminified UMD bundle
- `engine.io.min.js`: minified UMD bundle
- `engine.io.esm.min.js`: ESM bundle
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
### Features
* provide an ESM build without debug ([00d7e7d](https://github.com/socketio/engine.io-client/commit/00d7e7d7ee85b4cfa6f9f547203cc692083ac61c))
### BREAKING CHANGES
* the enableXDR option is removed ([c656192](https://github.com/socketio/engine.io-client/commit/c6561928be628084fd2f5e7a70943c8e5c582873))
* the jsonp and forceJSONP options are removed ([b2c7381](https://github.com/socketio/engine.io-client/commit/b2c73812e978489b5dfbe516a26b6b8fd628856d))
`ws` version: `~8.2.3`
# [5.2.0](https://github.com/socketio/engine.io-client/compare/5.1.2...5.2.0) (2021-08-29)
### Features
* add an option to use native timer functions ([#672](https://github.com/socketio/engine.io-client/issues/672)) ([5d1d5be](https://github.com/socketio/engine.io-client/commit/5d1d5bea11ab6854473ddc02a3391929ea4fc8f4))
## [5.1.2](https://github.com/socketio/engine.io-client/compare/5.1.1...5.1.2) (2021-06-24)
### Bug Fixes
* emit ping when receiving a ping from the server ([589d3ad](https://github.com/socketio/engine.io-client/commit/589d3ad63840329b5a61186603a415c534f8d4fc))
* **websocket:** fix timer blocking writes ([#670](https://github.com/socketio/engine.io-client/issues/670)) ([f30a10b](https://github.com/socketio/engine.io-client/commit/f30a10b7f45517fcb3abd02511c58a89e0ef498f))
## [5.1.1](https://github.com/socketio/engine.io-client/compare/5.1.0...5.1.1) (2021-05-11)
### Bug Fixes
* fix JSONP transport on IE9 ([bddd992](https://github.com/socketio/engine.io-client/commit/bddd9928fcdb33c79e0289bcafef337359dee12b))
## [4.1.4](https://github.com/socketio/engine.io-client/compare/4.1.3...4.1.4) (2021-05-05)
This release only contains a bump of `xmlhttprequest-ssl`, in order to fix the following vulnerability: https://www.npmjs.com/advisories/1665.
Please note that `engine.io-client` was not directly impacted by this vulnerability, since we are always using `async: true`.
## [3.5.2](https://github.com/socketio/engine.io-client/compare/3.5.1...3.5.2) (2021-05-05)
This release only contains a bump of `xmlhttprequest-ssl`, in order to fix the following vulnerability: https://www.npmjs.com/advisories/1665.
Please note that `engine.io-client` was not directly impacted by this vulnerability, since we are always using `async: true`.
# [5.1.0](https://github.com/socketio/engine.io-client/compare/5.0.1...5.1.0) (2021-05-04)
### Features
* add the "closeOnBeforeunload" option ([dcb85e9](https://github.com/socketio/engine.io-client/commit/dcb85e902d129b2d1a94943b4f6d471532f70dc9))
## [5.0.1](https://github.com/socketio/engine.io-client/compare/5.0.0...5.0.1) (2021-03-31)
### Bug Fixes
* ignore packets when the transport is silently closed ([d291a4c](https://github.com/socketio/engine.io-client/commit/d291a4c9f6accfc86fcd96683a5d493a87e3644c))
# [5.0.0](https://github.com/socketio/engine.io-client/compare/4.1.2...5.0.0) (2021-03-10)
The major bump is due to a breaking change on the server side.
### Features
* add autoUnref option ([6551683](https://github.com/socketio/engine.io-client/commit/65516836b2b6fe28d80e9a5918f9e10baa7451d8))
* listen to the "offline" event ([c361bc6](https://github.com/socketio/engine.io-client/commit/c361bc691f510b96f8909c5e6c62a4635d50275c))
## [3.5.1](https://github.com/socketio/engine.io-client/compare/3.5.0...3.5.1) (2021-03-02)
### Bug Fixes
* replace default nulls in SSL options with undefineds ([d0c551c](https://github.com/socketio/engine.io-client/commit/d0c551cca1e37301e8b28843c8f6e7ad5cf561d3))
## [4.1.2](https://github.com/socketio/engine.io-client/compare/4.1.1...4.1.2) (2021-02-25)
### Bug Fixes
* silently close the transport in the beforeunload hook ([ed48b5d](https://github.com/socketio/engine.io-client/commit/ed48b5dc3407e5ded45072606b3bb0eafa49c01f))
## [4.1.1](https://github.com/socketio/engine.io-client/compare/4.1.0...4.1.1) (2021-02-02)
### Bug Fixes
* remove polyfill for process in the bundle ([c95fdea](https://github.com/socketio/engine.io-client/commit/c95fdea83329b264964641bb48e3be2a8772f7a1))
# [4.1.0](https://github.com/socketio/engine.io-client/compare/4.0.6...4.1.0) (2021-01-14)
### Features
* add missing ws options ([d134fee](https://github.com/socketio/engine.io-client/commit/d134feeaa615afc4cbe0aa45aa4344c899b65df0))
## [4.0.6](https://github.com/socketio/engine.io-client/compare/4.0.5...4.0.6) (2021-01-04)
# [3.5.0](https://github.com/socketio/engine.io-client/compare/3.4.4...3.5.0) (2020-12-30)
### Bug Fixes
* check the type of the initial packet ([8750356](https://github.com/socketio/engine.io-client/commit/8750356dba5409ba0e1d3a27da6d214118702b3e))
## [4.0.5](https://github.com/socketio/engine.io-client/compare/4.0.4...4.0.5) (2020-12-07)
## [4.0.4](https://github.com/socketio/engine.io-client/compare/4.0.3...4.0.4) (2020-11-17)
### Bug Fixes
* check the type of the initial packet ([1c8cba8](https://github.com/socketio/engine.io-client/commit/1c8cba8818e930205918a70f05c1164865842a48))
* restore the cherry-picking of the WebSocket options ([4873a23](https://github.com/socketio/engine.io-client/commit/4873a237f1ce5fcb18e255dd604d50dcfc624ea8))
## [4.0.3](https://github.com/socketio/engine.io-client/compare/4.0.2...4.0.3) (2020-11-17)
### Bug Fixes
* **react-native:** add a default value for the withCredentials option ([ccb99e3](https://github.com/socketio/engine.io-client/commit/ccb99e3718e8ee2c50960430d2bd6c12a3dcb0dc))
* **react-native:** exclude the localAddress option ([177b95f](https://github.com/socketio/engine.io-client/commit/177b95fe463ad049b35170f042a771380fdaedee))
## [4.0.2](https://github.com/socketio/engine.io-client/compare/4.0.1...4.0.2) (2020-11-09)
## [4.0.1](https://github.com/socketio/engine.io-client/compare/4.0.0...4.0.1) (2020-10-21)
## [3.4.4](https://github.com/socketio/engine.io-client/compare/3.4.3...3.4.4) (2020-09-30)
# [4.0.0](https://github.com/socketio/engine.io-client/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
* **react-native:** restrict the list of options for the WebSocket object ([2f5c948](https://github.com/socketio/engine.io-client/commit/2f5c948abe8fd1c0fdb010e88f96bd933a3792ea))
* use globalThis polyfill instead of self/global ([#634](https://github.com/socketio/engine.io-client/issues/634)) ([3f3a6f9](https://github.com/socketio/engine.io-client/commit/3f3a6f991404ef601252193382d2d2029cff6c45))
### Features
* strip debug from the browser bundle ([f7ba966](https://github.com/socketio/engine.io-client/commit/f7ba966e53f4609f755880be8fa504f7252b0817))
#### Links
- Diff: [v4.0.0-alpha.1...4.0.0](https://github.com/socketio/engine.io-client/compare/v4.0.0-alpha.1...4.0.0)
- Full diff: [3.4.0...4.0.0](https://github.com/socketio/engine.io-client/compare/3.4.0...4.0.0)
- Server release: [4.0.0](https://github.com/socketio/engine.io/releases/tag/4.0.0)
- ws version: [~7.2.1](https://github.com/websockets/ws/releases/tag/7.2.1)
## [3.4.1](https://github.com/socketio/engine.io-client/compare/3.4.0...3.4.1) (2020-04-17)
### Bug Fixes
* use globalThis polyfill instead of self/global ([357f01d](https://github.com/socketio/engine.io-client/commit/357f01d90448d8565b650377bc7cabb351d991bd))
#### Links
- Diff: [3.4.0...3.4.1](https://github.com/socketio/engine.io-client/compare/3.4.0...3.4.1)
- Server release: [3.4.1](https://github.com/socketio/engine.io/releases/tag/3.4.1)
- ws version: [~6.1.0](https://github.com/websockets/ws/releases/tag/6.1.0)
# [4.0.0-alpha.1](https://github.com/socketio/engine.io-client/compare/v4.0.0-alpha.0...v4.0.0-alpha.1) (2020-02-12)
### Bug Fixes
* properly assign options when creating the transport ([7c7f1a9](https://github.com/socketio/engine.io-client/commit/7c7f1a9fe24856e3a155db1dc67d12d1586ffa37))
#### 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)
- Server release: [v4.0.0-alpha.1](https://github.com/socketio/engine.io/releases/tag/v4.0.0-alpha.1)
- ws version: [~7.2.1](https://github.com/websockets/ws/releases/tag/7.2.1)
# [4.0.0-alpha.0](https://github.com/socketio/engine.io-client/compare/3.4.0...v4.0.0-alpha.0) (2020-02-12)
### chore
* migrate to webpack 4 ([11dc4f3](https://github.com/socketio/engine.io-client/commit/11dc4f3a56d440f24b8a091485fef038d592bd6e))
### Features
* reverse the ping-pong mechanism ([81d7171](https://github.com/socketio/engine.io-client/commit/81d7171c6bb4053c802e3cc4b29a0e42dcf9c065))
### BREAKING CHANGES
* v3.x clients will not be able to connect anymore (they
will send a ping packet and timeout while waiting for a pong packet).
* the output bundle will now be found in the dist/ folder.
#### 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)
- Server release: [v4.0.0-alpha.0](https://github.com/socketio/engine.io/releases/tag/v4.0.0-alpha.0)
- ws version: [~7.2.1](https://github.com/websockets/ws/releases/tag/7.2.1)

View File

@@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2014-2015 Automattic <dev@cloudup.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,331 @@
# Engine.IO client
[![Build Status](https://github.com/socketio/engine.io-client/workflows/CI/badge.svg?branch=main)](https://github.com/socketio/engine.io-client/actions)
[![NPM version](https://badge.fury.io/js/engine.io-client.svg)](http://badge.fury.io/js/engine.io-client)
This is the client for [Engine.IO](http://github.com/socketio/engine.io),
the implementation of transport-based cross-browser/cross-device
bi-directional communication layer for [Socket.IO](http://github.com/socketio/socket.io).
## How to use
### Standalone
You can find an `engine.io.js` file in this repository, which is a
standalone build you can use as follows:
```html
<script src="/path/to/engine.io.js"></script>
<script>
// eio = Socket
const socket = eio('ws://localhost');
socket.on('open', () => {
socket.on('message', (data) => {});
socket.on('close', () => {});
});
</script>
```
### With browserify
Engine.IO is a commonjs module, which means you can include it by using
`require` on the browser and package using [browserify](http://browserify.org/):
1. install the client package
```bash
$ npm install engine.io-client
```
1. write your app code
```js
const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost');
socket.on('open', () => {
socket.on('message', (data) => {});
socket.on('close', () => {});
});
```
1. build your app bundle
```bash
$ browserify app.js > bundle.js
```
1. include on your page
```html
<script src="/path/to/bundle.js"></script>
```
### Sending and receiving binary
```html
<script src="/path/to/engine.io.js"></script>
<script>
const socket = eio('ws://localhost/');
socket.binaryType = 'blob';
socket.on('open', () => {
socket.send(new Int8Array(5));
socket.on('message', (blob) => {});
socket.on('close', () => {});
});
</script>
```
### Node.JS
Add `engine.io-client` to your `package.json` and then:
```js
const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost');
socket.on('open', () => {
socket.on('message', (data) => {});
socket.on('close', () => {});
});
```
### Node.js with certificates
```js
const opts = {
key: fs.readFileSync('test/fixtures/client.key'),
cert: fs.readFileSync('test/fixtures/client.crt'),
ca: fs.readFileSync('test/fixtures/ca.crt')
};
const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost', opts);
socket.on('open', () => {
socket.on('message', (data) => {});
socket.on('close', () => {});
});
```
### Node.js with extraHeaders
```js
const opts = {
extraHeaders: {
'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 { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost', opts);
socket.on('open', () => {
socket.on('message', (data) => {});
socket.on('close', () => {});
});
```
In the browser, the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object does not support additional headers.
In case you want to add some headers as part of some authentication mechanism, you can use the `transportOptions` attribute.
Please note that in this case the headers won't be sent in the WebSocket upgrade request.
```js
// WILL NOT WORK in the browser
const socket = new Socket('http://localhost', {
extraHeaders: {
'X-Custom-Header-For-My-Project': 'will not be sent'
}
});
// WILL NOT WORK
const socket = new Socket('http://localhost', {
transports: ['websocket'], // polling is disabled
transportOptions: {
polling: {
extraHeaders: {
'X-Custom-Header-For-My-Project': 'will not be sent'
}
}
}
});
// WILL WORK
const socket = new Socket('http://localhost', {
transports: ['polling', 'websocket'],
transportOptions: {
polling: {
extraHeaders: {
'X-Custom-Header-For-My-Project': 'will be used'
}
}
}
});
```
## Features
- Lightweight
- Runs on browser and node.js seamlessly
- Transports are independent of `Engine`
- Easy to debug
- Easy to unit test
- Runs inside HTML5 WebWorker
- Can send and receive binary data
- Receives as ArrayBuffer or Blob when in browser, and Buffer or ArrayBuffer
in Node
- When XHR2 or WebSockets are used, binary is emitted directly. Otherwise
binary is encoded into base64 strings, and decoded when binary types are
supported.
- With browsers that don't support ArrayBuffer, an object { base64: true,
data: dataAsBase64String } is emitted on the `message` event.
## API
### Socket
The client class. Mixes in [Emitter](http://github.com/component/emitter).
Exposed as `eio` in the browser standalone build.
#### Properties
- `protocol` _(Number)_: protocol revision number
- `binaryType` _(String)_ : can be set to 'arraybuffer' or 'blob' in browsers,
and `buffer` or `arraybuffer` in Node. Blob is only used in browser if it's
supported.
#### Events
- `open`
- Fired upon successful connection.
- `message`
- Fired when data is received from the server.
- **Arguments**
- `String` | `ArrayBuffer`: utf-8 encoded data or ArrayBuffer containing
binary data
- `close`
- Fired upon disconnection. In compliance with the WebSocket API spec, this event may be
fired even if the `open` event does not occur (i.e. due to connection error or `close()`).
- `error`
- Fired when an error occurs.
- `flush`
- Fired upon completing a buffer flush
- `drain`
- Fired after `drain` event of transport if writeBuffer is empty
- `upgradeError`
- Fired if an error occurs with a transport we're trying to upgrade to.
- `upgrade`
- Fired upon upgrade success, after the new transport is set
- `ping`
- Fired upon receiving a ping packet.
- `pong`
- Fired upon _flushing_ a pong packet (ie: actual packet write out)
#### Methods
- **constructor**
- Initializes the client
- **Parameters**
- `String` uri
- `Object`: optional, options object
- **Options**
- `agent` (`http.Agent`): `http.Agent` to use, defaults to `false` (NodeJS only)
- `upgrade` (`Boolean`): defaults to true, whether the client should try
to upgrade the transport from long-polling to something better.
- `forceBase64` (`Boolean`): forces base 64 encoding for polling transport even when XHR2 responseType is available and WebSocket even if the used standard supports binary.
- `withCredentials` (`Boolean`): defaults to `false`, whether to include credentials (cookies, authorization headers, TLS client certificates, etc.) with cross-origin XHR polling requests.
- `timestampRequests` (`Boolean`): whether to add the timestamp with each
transport request. Note: polling requests are always stamped unless this
option is explicitly set to `false` (`false`)
- `timestampParam` (`String`): timestamp parameter (`t`)
- `path` (`String`): path to connect to, default is `/engine.io`
- `transports` (`Array`): a list of transports to try (in order).
Defaults to `['polling', 'websocket', 'webtransport']`. `Engine`
always attempts to connect directly with the first one, provided the
feature detection test for it passes.
- `transportOptions` (`Object`): hash of options, indexed by transport name, overriding the common options for the given transport
- `rememberUpgrade` (`Boolean`): defaults to false.
If true and if the previous websocket connection to the server succeeded,
the connection attempt will bypass the normal upgrade process and will initially
try websocket. A connection attempt following a transport error will use the
normal upgrade process. It is recommended you turn this on only when using
SSL/TLS connections, or if you know that your network does not block websockets.
- `pfx` (`String`|`Buffer`): Certificate, Private key and CA certificates to use for SSL. Can be used in Node.js client environment to manually specify certificate information.
- `key` (`String`): Private key to use for SSL. Can be used in Node.js client environment to manually specify certificate information.
- `passphrase` (`String`): A string of passphrase for the private key or pfx. Can be used in Node.js client environment to manually specify certificate information.
- `cert` (`String`): Public x509 certificate to use. Can be used in Node.js client environment to manually specify certificate information.
- `ca` (`String`|`Array`): An authority certificate or array of authority certificates to check the remote host against.. Can be used in Node.js client environment to manually specify certificate information.
- `ciphers` (`String`): A string describing the ciphers to use or exclude. Consult the [cipher format list](http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT) for details on the format. Can be used in Node.js client environment to manually specify certificate information.
- `rejectUnauthorized` (`Boolean`): If true, the server certificate is verified against the list of supplied CAs. An 'error' event is emitted if verification fails. Verification happens at the connection level, before the HTTP request is sent. Can be used in Node.js client environment to manually specify certificate information.
- `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`)
- `threshold` (`Number`): data is compressed only if the byte size is above this value. This option is ignored on the browser. (`1024`)
- `extraHeaders` (`Object`): Headers that will be passed for each request to the server (via xhr-polling and via websockets). These values then can be used during handshake or for special proxies. Can only be used in Node.js client environment.
- `localAddress` (`String`): the local IP address to connect to
- `autoUnref` (`Boolean`): whether the transport should be `unref`'d upon creation. This calls `unref` on the underlying timers and sockets so that the program is allowed to exit if they are the only timers/sockets in the event system (Node.js only)
- `useNativeTimers` (`Boolean`): Whether to always use the native timeouts. This allows the client to reconnect when the native timeout functions are overridden, such as when mock clocks are installed with [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers).
- **Polling-only options**
- `requestTimeout` (`Number`): Timeout for xhr-polling requests in milliseconds (`0`)
- **Websocket-only options**
- `protocols` (`Array`): a list of subprotocols (see [MDN reference](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Subprotocols))
- `closeOnBeforeunload` (`Boolean`): whether to silently close the connection when the [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) event is emitted in the browser (defaults to `false`)
- `send`
- Sends a message to the server
- **Parameters**
- `String` | `ArrayBuffer` | `ArrayBufferView` | `Blob`: data to send
- `Object`: optional, options object
- `Function`: optional, callback upon `drain`
- **Options**
- `compress` (`Boolean`): whether to compress sending data. This option is ignored and forced to be `true` on the browser. (`true`)
- `close`
- Disconnects the client.
### Transport
The transport class. Private. _Inherits from EventEmitter_.
#### Events
- `poll`: emitted by polling transports upon starting a new request
- `pollComplete`: emitted by polling transports upon completing a request
- `drain`: emitted by polling transports upon a buffer drain
## Tests
`engine.io-client` is used to test
[engine](http://github.com/socketio/engine.io). Running the `engine.io`
test suite ensures the client works and vice-versa.
Browser tests are run using [zuul](https://github.com/defunctzombie/zuul). You can
run the tests locally using the following command.
```
./node_modules/.bin/zuul --local 8080 -- test/index.js
```
Additionally, `engine.io-client` has a standalone test suite you can run
with `make test` which will run node.js and browser tests. You must have zuul setup with
a saucelabs account.
## Support
The support channels for `engine.io-client` are the same as `socket.io`:
- irc.freenode.net **#socket.io**
- [Google Groups](http://groups.google.com/group/socket_io)
- [Website](http://socket.io)
## Development
To contribute patches, run tests or benchmarks, make sure to clone the
repository:
```bash
git clone git://github.com/socketio/engine.io-client.git
```
Then:
```bash
cd engine.io-client
npm install
```
See the `Tests` section above for how to run tests before submitting any patches.
## License
MIT - Copyright (c) 2014 Automattic, Inc.

View File

@@ -0,0 +1,22 @@
# Security Policy
## Supported Versions
| Version | `socket.io-client` version | Supported |
|---------|----------------------------|--------------------|
| 6.x | 4.x | :white_check_mark: |
| 4.x | 3.x | :white_check_mark: |
| 3.5.x | 2.4.x | :white_check_mark: |
| < 3.5.0 | < 2.4.0 | :x: |
## Reporting a Vulnerability
To report a security vulnerability in this package, please send an email to [@darrachequesne](https://github.com/darrachequesne) (see address in profile) describing the vulnerability and how to reproduce it.
We will get back to you as soon as possible and publish a fix if necessary.
:warning: IMPORTANT :warning: please do not create an issue in this repository, as attackers might take advantage of it. Thank you in advance for your responsible disclosure.
## History
- Mar 2016: [Insecure Defaults Allow MITM Over TLS in engine.io-client](https://github.com/advisories/GHSA-4r4m-hjwj-43p8) (CVE-2016-10536)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import { Socket } from "./socket.js";
export default (uri, opts) => new Socket(uri, opts);

View File

@@ -0,0 +1,12 @@
// imported from https://github.com/component/has-cors
let value = false;
try {
value = typeof XMLHttpRequest !== 'undefined' &&
'withCredentials' in new XMLHttpRequest();
} catch (err) {
// if XMLHttp support is disabled in IE then it will throw
// when trying to create
}
export const hasCORS = value;

View File

@@ -0,0 +1,38 @@
// imported from https://github.com/galkn/querystring
/**
* Compiles a querystring
* Returns string representation of the object
*
* @param {Object}
* @api private
*/
export function encode (obj) {
let str = '';
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
if (str.length) str += '&';
str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]);
}
}
return str;
}
/**
* Parses a simple querystring into an object
*
* @param {String} qs
* @api private
*/
export function decode (qs) {
let qry = {};
let pairs = qs.split('&');
for (let i = 0, l = pairs.length; i < l; i++) {
let pair = pairs[i].split('=');
qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
}
return qry;
}

View File

@@ -0,0 +1,84 @@
// imported from https://github.com/galkn/parseuri
/**
* Parses a URI
*
* Note: we could also have used the built-in URL object, but it isn't supported on all platforms.
*
* See:
* - https://developer.mozilla.org/en-US/docs/Web/API/URL
* - https://caniuse.com/url
* - https://www.rfc-editor.org/rfc/rfc3986#appendix-B
*
* History of the parse() method:
* - first commit: https://github.com/socketio/socket.io-client/commit/4ee1d5d94b3906a9c052b459f1a818b15f38f91c
* - export into its own module: https://github.com/socketio/engine.io-client/commit/de2c561e4564efeb78f1bdb1ba39ef81b2822cb3
* - reimport: https://github.com/socketio/engine.io-client/commit/df32277c3f6d622eec5ed09f493cae3f3391d242
*
* @author Steven Levithan <stevenlevithan.com> (MIT license)
* @api private
*/
const re = /^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
const parts = [
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
];
export function parse(str: string) {
if (str.length > 8000) {
throw "URI too long";
}
const src = str,
b = str.indexOf('['),
e = str.indexOf(']');
if (b != -1 && e != -1) {
str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length);
}
let m = re.exec(str || ''),
uri = {} as any,
i = 14;
while (i--) {
uri[parts[i]] = m[i] || '';
}
if (b != -1 && e != -1) {
uri.source = src;
uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':');
uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':');
uri.ipv6uri = true;
}
uri.pathNames = pathNames(uri, uri['path']);
uri.queryKey = queryKey(uri, uri['query']);
return uri;
}
function pathNames(obj, path) {
const regx = /\/{2,9}/g,
names = path.replace(regx, "/").split("/");
if (path.slice(0, 1) == '/' || path.length === 0) {
names.splice(0, 1);
}
if (path.slice(-1) == '/') {
names.splice(names.length - 1, 1);
}
return names;
}
function queryKey(uri, query) {
const data = {};
query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {
if ($1) {
data[$1] = $2;
}
});
return data;
}

View File

@@ -0,0 +1,113 @@
export const nextTick = process.nextTick;
export const globalThisShim = global;
export const defaultBinaryType = "nodebuffer";
export function createCookieJar() {
return new CookieJar();
}
interface Cookie {
name: string;
value: string;
expires?: Date;
}
/**
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
*/
export function parse(setCookieString: string): Cookie {
const parts = setCookieString.split("; ");
const i = parts[0].indexOf("=");
if (i === -1) {
return;
}
const name = parts[0].substring(0, i).trim();
if (!name.length) {
return;
}
let value = parts[0].substring(i + 1).trim();
if (value.charCodeAt(0) === 0x22) {
// remove double quotes
value = value.slice(1, -1);
}
const cookie: Cookie = {
name,
value,
};
for (let j = 1; j < parts.length; j++) {
const subParts = parts[j].split("=");
if (subParts.length !== 2) {
continue;
}
const key = subParts[0].trim();
const value = subParts[1].trim();
switch (key) {
case "Expires":
cookie.expires = new Date(value);
break;
case "Max-Age":
const expiration = new Date();
expiration.setUTCSeconds(
expiration.getUTCSeconds() + parseInt(value, 10)
);
cookie.expires = expiration;
break;
default:
// ignore other keys
}
}
return cookie;
}
export class CookieJar {
private _cookies = new Map<string, Cookie>();
public parseCookies(values: string[]) {
if (!values) {
return;
}
values.forEach((value) => {
const parsed = parse(value);
if (parsed) {
this._cookies.set(parsed.name, parsed);
}
});
}
get cookies() {
const now = Date.now();
this._cookies.forEach((cookie, name) => {
if (cookie.expires?.getTime() < now) {
this._cookies.delete(name);
}
});
return this._cookies.entries();
}
public addCookies(xhr: any) {
const cookies = [];
for (const [name, cookie] of this.cookies) {
cookies.push(`${name}=${cookie.value}`);
}
if (cookies.length) {
xhr.setDisableHeaderCheck(true);
xhr.setRequestHeader("cookie", cookies.join("; "));
}
}
public appendCookies(headers: Headers) {
for (const [name, cookie] of this.cookies) {
headers.append("cookie", `${name}=${cookie.value}`);
}
}
}

View File

@@ -0,0 +1,23 @@
export const nextTick = (() => {
const isPromiseAvailable =
typeof Promise === "function" && typeof Promise.resolve === "function";
if (isPromiseAvailable) {
return (cb) => Promise.resolve().then(cb);
} else {
return (cb, setTimeoutFn) => setTimeoutFn(cb, 0);
}
})();
export const globalThisShim = (() => {
if (typeof self !== "undefined") {
return self;
} else if (typeof window !== "undefined") {
return window;
} else {
return Function("return this")();
}
})();
export const defaultBinaryType = "arraybuffer";
export function createCookieJar() {}

View File

@@ -0,0 +1,21 @@
import { Socket } from "./socket.js";
export { Socket };
export {
SocketOptions,
SocketWithoutUpgrade,
SocketWithUpgrade,
} from "./socket.js";
export const protocol = Socket.protocol;
export { Transport, TransportError } from "./transport.js";
export { transports } from "./transports/index.js";
export { installTimerFunctions } from "./util.js";
export { parse } from "./contrib/parseuri.js";
export { nextTick } from "./globals.node.js";
export { Fetch } from "./transports/polling-fetch.js";
export { XHR as NodeXHR } from "./transports/polling-xhr.node.js";
export { XHR } from "./transports/polling-xhr.js";
export { WS as NodeWebSocket } from "./transports/websocket.node.js";
export { WS as WebSocket } from "./transports/websocket.js";
export { WT as WebTransport } from "./transports/webtransport.js";

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
import { decodePacket } from "engine.io-parser";
import type { Packet, RawData } from "engine.io-parser";
import { Emitter } from "@socket.io/component-emitter";
import { installTimerFunctions } from "./util.js";
import type { Socket, SocketOptions } from "./socket.js";
import { encode } from "./contrib/parseqs.js";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:transport"); // debug()
export class TransportError extends Error {
public readonly type = "TransportError";
constructor(
reason: string,
readonly description: any,
readonly context: any
) {
super(reason);
}
}
export interface CloseDetails {
description: string;
context?: unknown; // context should be typed as CloseEvent | XMLHttpRequest, but these types are not available on non-browser platforms
}
interface TransportReservedEvents {
open: () => void;
error: (err: TransportError) => void;
packet: (packet: Packet) => void;
close: (details?: CloseDetails) => void;
poll: () => void;
pollComplete: () => void;
drain: () => void;
}
type TransportState = "opening" | "open" | "closed" | "pausing" | "paused";
export abstract class Transport extends Emitter<
Record<never, never>,
Record<never, never>,
TransportReservedEvents
> {
public query: Record<string, string>;
public writable: boolean = false;
protected opts: SocketOptions;
protected supportsBinary: boolean;
protected readyState: TransportState;
protected socket: Socket;
protected setTimeoutFn: typeof setTimeout;
/**
* Transport abstract constructor.
*
* @param {Object} opts - options
* @protected
*/
constructor(opts) {
super();
installTimerFunctions(this, opts);
this.opts = opts;
this.query = opts.query;
this.socket = opts.socket;
this.supportsBinary = !opts.forceBase64;
}
/**
* Emits an error.
*
* @param {String} reason
* @param description
* @param context - the error context
* @return {Transport} for chaining
* @protected
*/
protected onError(reason: string, description: any, context?: any) {
super.emitReserved(
"error",
new TransportError(reason, description, context)
);
return this;
}
/**
* Opens the transport.
*/
public open() {
this.readyState = "opening";
this.doOpen();
return this;
}
/**
* Closes the transport.
*/
public close() {
if (this.readyState === "opening" || this.readyState === "open") {
this.doClose();
this.onClose();
}
return this;
}
/**
* Sends multiple packets.
*
* @param {Array} packets
*/
public send(packets) {
if (this.readyState === "open") {
this.write(packets);
} else {
// this might happen if the transport was silently closed in the beforeunload event handler
debug("transport is not open, discarding packets");
}
}
/**
* Called upon open
*
* @protected
*/
protected onOpen() {
this.readyState = "open";
this.writable = true;
super.emitReserved("open");
}
/**
* Called with data.
*
* @param {String} data
* @protected
*/
protected onData(data: RawData) {
const packet = decodePacket(data, this.socket.binaryType);
this.onPacket(packet);
}
/**
* Called with a decoded packet.
*
* @protected
*/
protected onPacket(packet: Packet) {
super.emitReserved("packet", packet);
}
/**
* Called upon close.
*
* @protected
*/
protected onClose(details?: CloseDetails) {
this.readyState = "closed";
super.emitReserved("close", details);
}
/**
* The name of the transport
*/
public abstract get name(): string;
/**
* Pauses the transport, in order not to lose packets during an upgrade.
*
* @param onPause
*/
public pause(onPause: () => void) {}
protected createUri(schema: string, query: Record<string, unknown> = {}) {
return (
schema +
"://" +
this._hostname() +
this._port() +
this.opts.path +
this._query(query)
);
}
private _hostname() {
const hostname = this.opts.hostname;
return hostname.indexOf(":") === -1 ? hostname : "[" + hostname + "]";
}
private _port() {
if (
this.opts.port &&
((this.opts.secure && Number(this.opts.port !== 443)) ||
(!this.opts.secure && Number(this.opts.port) !== 80))
) {
return ":" + this.opts.port;
} else {
return "";
}
}
private _query(query: Record<string, unknown>) {
const encodedQuery = encode(query);
return encodedQuery.length ? "?" + encodedQuery : "";
}
protected abstract doOpen();
protected abstract doClose();
protected abstract write(packets: Packet[]);
}

View File

@@ -0,0 +1,9 @@
import { XHR } from "./polling-xhr.node.js";
import { WS } from "./websocket.node.js";
import { WT } from "./webtransport.js";
export const transports = {
websocket: WS,
webtransport: WT,
polling: XHR,
};

View File

@@ -0,0 +1,64 @@
import { Polling } from "./polling.js";
import { CookieJar, createCookieJar } from "../globals.node.js";
/**
* HTTP long-polling based on the built-in `fetch()` method.
*
* Usage: browser, Node.js (since v18), Deno, Bun
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/fetch
* @see https://caniuse.com/fetch
* @see https://nodejs.org/api/globals.html#fetch
*/
export class Fetch extends Polling {
override doPoll() {
this._fetch()
.then((res) => {
if (!res.ok) {
return this.onError("fetch read error", res.status, res);
}
res.text().then((data) => this.onData(data));
})
.catch((err) => {
this.onError("fetch read error", err);
});
}
override doWrite(data: string, callback: () => void) {
this._fetch(data)
.then((res) => {
if (!res.ok) {
return this.onError("fetch write error", res.status, res);
}
callback();
})
.catch((err) => {
this.onError("fetch write error", err);
});
}
private _fetch(data?: string) {
const isPost = data !== undefined;
const headers = new Headers(this.opts.extraHeaders);
if (isPost) {
headers.set("content-type", "text/plain;charset=UTF-8");
}
this.socket._cookieJar?.appendCookies(headers);
return fetch(this.uri(), {
method: isPost ? "POST" : "GET",
body: isPost ? data : null,
headers,
credentials: this.opts.withCredentials ? "include" : "omit",
}).then((res) => {
// @ts-ignore getSetCookie() was added in Node.js v19.7.0
this.socket._cookieJar?.parseCookies(res.headers.getSetCookie());
return res;
});
}
}

View File

@@ -0,0 +1,26 @@
import * as XMLHttpRequestModule from "xmlhttprequest-ssl";
import { BaseXHR, Request, RequestOptions } from "./polling-xhr.js";
const XMLHttpRequest = XMLHttpRequestModule.default || XMLHttpRequestModule;
/**
* HTTP long-polling based on the `XMLHttpRequest` object provided by the `xmlhttprequest-ssl` package.
*
* Usage: Node.js, Deno (compat), Bun (compat)
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
*/
export class XHR extends BaseXHR {
request(opts: Record<string, any> = {}) {
Object.assign(
opts,
{ xd: this.xd, cookieJar: this.socket?._cookieJar },
this.opts
);
return new Request(
(opts) => new XMLHttpRequest(opts),
this.uri(),
opts as RequestOptions
);
}
}

View File

@@ -0,0 +1,361 @@
import { Polling } from "./polling.js";
import { Emitter } from "@socket.io/component-emitter";
import type { SocketOptions } from "../socket.js";
import { installTimerFunctions, pick } from "../util.js";
import { globalThisShim as globalThis } from "../globals.node.js";
import type { CookieJar } from "../globals.node.js";
import type { RawData } from "engine.io-parser";
import { hasCORS } from "../contrib/has-cors.js";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:polling"); // debug()
function empty() {}
export abstract class BaseXHR extends Polling {
protected readonly xd: boolean;
private pollXhr: any;
/**
* XHR Polling constructor.
*
* @param {Object} opts
* @package
*/
constructor(opts) {
super(opts);
if (typeof location !== "undefined") {
const isSSL = "https:" === location.protocol;
let port = location.port;
// some user agents have empty `location.port`
if (!port) {
port = isSSL ? "443" : "80";
}
this.xd =
(typeof location !== "undefined" &&
opts.hostname !== location.hostname) ||
port !== opts.port;
}
}
/**
* Creates a request.
*
* @private
*/
abstract request(opts?: Record<string, any>);
/**
* Sends data.
*
* @param {String} data to send.
* @param {Function} called upon flush.
* @private
*/
override doWrite(data, fn) {
const req = this.request({
method: "POST",
data: data,
});
req.on("success", fn);
req.on("error", (xhrStatus, context) => {
this.onError("xhr post error", xhrStatus, context);
});
}
/**
* Starts a poll cycle.
*
* @private
*/
override doPoll() {
debug("xhr poll");
const req = this.request();
req.on("data", this.onData.bind(this));
req.on("error", (xhrStatus, context) => {
this.onError("xhr poll error", xhrStatus, context);
});
this.pollXhr = req;
}
}
interface RequestReservedEvents {
success: () => void;
data: (data: RawData) => void;
error: (err: number | Error, context: unknown) => void; // context should be typed as XMLHttpRequest, but this type is not available on non-browser platforms
}
export type RequestOptions = SocketOptions & {
method?: string;
data?: RawData;
xd: boolean;
cookieJar: CookieJar;
};
export class Request extends Emitter<
Record<never, never>,
Record<never, never>,
RequestReservedEvents
> {
private readonly _opts: RequestOptions;
private readonly _method: string;
private readonly _uri: string;
private readonly _data: string | ArrayBuffer;
private _xhr: any;
private setTimeoutFn: typeof setTimeout;
private _index: number;
static requestsCount = 0;
static requests = {};
/**
* Request constructor
*
* @param {Object} options
* @package
*/
constructor(
private readonly createRequest: (opts: RequestOptions) => XMLHttpRequest,
uri: string,
opts: RequestOptions
) {
super();
installTimerFunctions(this, opts);
this._opts = opts;
this._method = opts.method || "GET";
this._uri = uri;
this._data = undefined !== opts.data ? opts.data : null;
this._create();
}
/**
* Creates the XHR object and sends the request.
*
* @private
*/
private _create() {
const opts = pick(
this._opts,
"agent",
"pfx",
"key",
"passphrase",
"cert",
"ca",
"ciphers",
"rejectUnauthorized",
"autoUnref"
);
opts.xdomain = !!this._opts.xd;
const xhr = (this._xhr = this.createRequest(opts));
try {
debug("xhr open %s: %s", this._method, this._uri);
xhr.open(this._method, this._uri, true);
try {
if (this._opts.extraHeaders) {
// @ts-ignore
xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
for (let i in this._opts.extraHeaders) {
if (this._opts.extraHeaders.hasOwnProperty(i)) {
xhr.setRequestHeader(i, this._opts.extraHeaders[i]);
}
}
}
} catch (e) {}
if ("POST" === this._method) {
try {
xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
} catch (e) {}
}
try {
xhr.setRequestHeader("Accept", "*/*");
} catch (e) {}
this._opts.cookieJar?.addCookies(xhr);
// ie6 check
if ("withCredentials" in xhr) {
xhr.withCredentials = this._opts.withCredentials;
}
if (this._opts.requestTimeout) {
xhr.timeout = this._opts.requestTimeout;
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 3) {
this._opts.cookieJar?.parseCookies(
// @ts-ignore
xhr.getResponseHeader("set-cookie")
);
}
if (4 !== xhr.readyState) return;
if (200 === xhr.status || 1223 === xhr.status) {
this._onLoad();
} else {
// make sure the `error` event handler that's user-set
// does not throw in the same tick and gets caught here
this.setTimeoutFn(() => {
this._onError(typeof xhr.status === "number" ? xhr.status : 0);
}, 0);
}
};
debug("xhr data %s", this._data);
xhr.send(this._data);
} catch (e) {
// Need to defer since .create() is called directly from the constructor
// and thus the 'error' event can only be only bound *after* this exception
// occurs. Therefore, also, we cannot throw here at all.
this.setTimeoutFn(() => {
this._onError(e);
}, 0);
return;
}
if (typeof document !== "undefined") {
this._index = Request.requestsCount++;
Request.requests[this._index] = this;
}
}
/**
* Called upon error.
*
* @private
*/
private _onError(err: number | Error) {
this.emitReserved("error", err, this._xhr);
this._cleanup(true);
}
/**
* Cleans up house.
*
* @private
*/
private _cleanup(fromError?) {
if ("undefined" === typeof this._xhr || null === this._xhr) {
return;
}
this._xhr.onreadystatechange = empty;
if (fromError) {
try {
this._xhr.abort();
} catch (e) {}
}
if (typeof document !== "undefined") {
delete Request.requests[this._index];
}
this._xhr = null;
}
/**
* Called upon load.
*
* @private
*/
private _onLoad() {
const data = this._xhr.responseText;
if (data !== null) {
this.emitReserved("data", data);
this.emitReserved("success");
this._cleanup();
}
}
/**
* Aborts the request.
*
* @package
*/
public abort() {
this._cleanup();
}
}
/**
* Aborts pending requests when unloading the window. This is needed to prevent
* memory leaks (e.g. when using IE) and to ensure that no spurious error is
* emitted.
*/
if (typeof document !== "undefined") {
// @ts-ignore
if (typeof attachEvent === "function") {
// @ts-ignore
attachEvent("onunload", unloadHandler);
} else if (typeof addEventListener === "function") {
const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload";
addEventListener(terminationEvent, unloadHandler, false);
}
}
function unloadHandler() {
for (let i in Request.requests) {
if (Request.requests.hasOwnProperty(i)) {
Request.requests[i].abort();
}
}
}
const hasXHR2 = (function () {
const xhr = newRequest({
xdomain: false,
});
return xhr && xhr.responseType !== null;
})();
/**
* HTTP long-polling based on the built-in `XMLHttpRequest` object.
*
* Usage: browser
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
*/
export class XHR extends BaseXHR {
constructor(opts) {
super(opts);
const forceBase64 = opts && opts.forceBase64;
this.supportsBinary = hasXHR2 && !forceBase64;
}
request(opts: Record<string, any> = {}) {
Object.assign(opts, { xd: this.xd }, this.opts);
return new Request(newRequest, this.uri(), opts as RequestOptions);
}
}
function newRequest(opts) {
const xdomain = opts.xdomain;
// XMLHttpRequest can be disabled on IE
try {
if ("undefined" !== typeof XMLHttpRequest && (!xdomain || hasCORS)) {
return new XMLHttpRequest();
}
} catch (e) {}
if (!xdomain) {
try {
return new globalThis[["Active"].concat("Object").join("X")](
"Microsoft.XMLHTTP"
);
} catch (e) {}
}
}

View File

@@ -0,0 +1,179 @@
import { Transport } from "../transport.js";
import { randomString } from "../util.js";
import { encodePayload, decodePayload } from "engine.io-parser";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:polling"); // debug()
export abstract class Polling extends Transport {
private _polling: boolean = false;
override get name() {
return "polling";
}
/**
* Opens the socket (triggers polling). We write a PING message to determine
* when the transport is open.
*
* @protected
*/
override doOpen() {
this._poll();
}
/**
* Pauses polling.
*
* @param {Function} onPause - callback upon buffers are flushed and transport is paused
* @package
*/
override pause(onPause) {
this.readyState = "pausing";
const pause = () => {
debug("paused");
this.readyState = "paused";
onPause();
};
if (this._polling || !this.writable) {
let total = 0;
if (this._polling) {
debug("we are currently polling - waiting to pause");
total++;
this.once("pollComplete", function () {
debug("pre-pause polling complete");
--total || pause();
});
}
if (!this.writable) {
debug("we are currently writing - waiting to pause");
total++;
this.once("drain", function () {
debug("pre-pause writing complete");
--total || pause();
});
}
} else {
pause();
}
}
/**
* Starts polling cycle.
*
* @private
*/
private _poll() {
debug("polling");
this._polling = true;
this.doPoll();
this.emitReserved("poll");
}
/**
* Overloads onData to detect payloads.
*
* @protected
*/
override onData(data) {
debug("polling got data %s", data);
const callback = (packet) => {
// if its the first message we consider the transport open
if ("opening" === this.readyState && packet.type === "open") {
this.onOpen();
}
// if its a close packet, we close the ongoing requests
if ("close" === packet.type) {
this.onClose({ description: "transport closed by the server" });
return false;
}
// otherwise bypass onData and handle the message
this.onPacket(packet);
};
// decode payload
decodePayload(data, this.socket.binaryType).forEach(callback);
// if an event did not trigger closing
if ("closed" !== this.readyState) {
// if we got data we're not polling
this._polling = false;
this.emitReserved("pollComplete");
if ("open" === this.readyState) {
this._poll();
} else {
debug('ignoring poll - transport state "%s"', this.readyState);
}
}
}
/**
* For polling, send a close packet.
*
* @protected
*/
override doClose() {
const close = () => {
debug("writing close packet");
this.write([{ type: "close" }]);
};
if ("open" === this.readyState) {
debug("transport open - closing");
close();
} else {
// in case we're trying to close while
// handshaking is in progress (GH-164)
debug("transport not open - deferring close");
this.once("open", close);
}
}
/**
* Writes a packets payload.
*
* @param {Array} packets - data packets
* @protected
*/
override write(packets) {
this.writable = false;
encodePayload(packets, (data) => {
this.doWrite(data, () => {
this.writable = true;
this.emitReserved("drain");
});
});
}
/**
* Generates uri for connection.
*
* @private
*/
protected uri() {
const schema = this.opts.secure ? "https" : "http";
const query: { b64?: number; sid?: string } = this.query || {};
// cache busting is forced
if (false !== this.opts.timestampRequests) {
query[this.opts.timestampParam] = randomString();
}
if (!this.supportsBinary && !query.sid) {
query.b64 = 1;
}
return this.createUri(schema, query);
}
abstract doPoll();
abstract doWrite(data: string, callback: () => void);
}

View File

@@ -0,0 +1,50 @@
import { WebSocket } from "ws";
import type { Packet, RawData } from "engine.io-parser";
import { BaseWS } from "./websocket.js";
/**
* WebSocket transport based on the `WebSocket` object provided by the `ws` package.
*
* Usage: Node.js, Deno (compat), Bun (compat)
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
* @see https://caniuse.com/mdn-api_websocket
*/
export class WS extends BaseWS {
createSocket(
uri: string,
protocols: string | string[] | undefined,
opts: Record<string, any>
) {
if (this.socket?._cookieJar) {
opts.headers = opts.headers || {};
opts.headers.cookie =
typeof opts.headers.cookie === "string"
? [opts.headers.cookie]
: opts.headers.cookie || [];
for (const [name, cookie] of this.socket._cookieJar.cookies) {
opts.headers.cookie.push(`${name}=${cookie.value}`);
}
}
return new WebSocket(uri, protocols, opts);
}
doWrite(packet: Packet, data: RawData) {
const opts: { compress?: boolean } = {};
if (packet.options) {
opts.compress = packet.options.compress;
}
if (this.opts.perMessageDeflate) {
const len =
// @ts-ignore
"string" === typeof data ? Buffer.byteLength(data) : data.length;
if (len < this.opts.perMessageDeflate.threshold) {
opts.compress = false;
}
}
this.ws.send(data, opts);
}
}

View File

@@ -0,0 +1,181 @@
import { Transport } from "../transport.js";
import { pick, randomString } from "../util.js";
import { encodePacket } from "engine.io-parser";
import type { Packet, RawData } from "engine.io-parser";
import { globalThisShim as globalThis, nextTick } from "../globals.node.js";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:websocket"); // debug()
// detect ReactNative environment
const isReactNative =
typeof navigator !== "undefined" &&
typeof navigator.product === "string" &&
navigator.product.toLowerCase() === "reactnative";
export abstract class BaseWS extends Transport {
protected ws: any;
override get name() {
return "websocket";
}
override doOpen() {
const uri = this.uri();
const protocols = this.opts.protocols;
// React Native only supports the 'headers' option, and will print a warning if anything else is passed
const opts = isReactNative
? {}
: pick(
this.opts,
"agent",
"perMessageDeflate",
"pfx",
"key",
"passphrase",
"cert",
"ca",
"ciphers",
"rejectUnauthorized",
"localAddress",
"protocolVersion",
"origin",
"maxPayload",
"family",
"checkServerIdentity"
);
if (this.opts.extraHeaders) {
opts.headers = this.opts.extraHeaders;
}
try {
this.ws = this.createSocket(uri, protocols, opts);
} catch (err) {
return this.emitReserved("error", err);
}
this.ws.binaryType = this.socket.binaryType;
this.addEventListeners();
}
abstract createSocket(
uri: string,
protocols: string | string[] | undefined,
opts: Record<string, any>
);
/**
* Adds event listeners to the socket
*
* @private
*/
private addEventListeners() {
this.ws.onopen = () => {
if (this.opts.autoUnref) {
this.ws._socket.unref();
}
this.onOpen();
};
this.ws.onclose = (closeEvent) =>
this.onClose({
description: "websocket connection closed",
context: closeEvent,
});
this.ws.onmessage = (ev) => this.onData(ev.data);
this.ws.onerror = (e) => this.onError("websocket error", e);
}
override write(packets) {
this.writable = false;
// encodePacket efficient as it uses WS framing
// no need for encodePayload
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
const lastPacket = i === packets.length - 1;
encodePacket(packet, this.supportsBinary, (data) => {
// Sometimes the websocket has already been closed but the browser didn't
// have a chance of informing us about it yet, in that case send will
// throw an error
try {
this.doWrite(packet, data);
} catch (e) {
debug("websocket closed before onclose event");
}
if (lastPacket) {
// fake drain
// defer to next tick to allow Socket to clear writeBuffer
nextTick(() => {
this.writable = true;
this.emitReserved("drain");
}, this.setTimeoutFn);
}
});
}
}
abstract doWrite(packet: Packet, data: RawData);
override doClose() {
if (typeof this.ws !== "undefined") {
this.ws.close();
this.ws = null;
}
}
/**
* Generates uri for connection.
*
* @private
*/
private uri() {
const schema = this.opts.secure ? "wss" : "ws";
const query: { b64?: number } = this.query || {};
// append timestamp to URI
if (this.opts.timestampRequests) {
query[this.opts.timestampParam] = randomString();
}
// communicate binary support capabilities
if (!this.supportsBinary) {
query.b64 = 1;
}
return this.createUri(schema, query);
}
}
const WebSocketCtor = globalThis.WebSocket || globalThis.MozWebSocket;
/**
* WebSocket transport based on the built-in `WebSocket` object.
*
* Usage: browser, Node.js (since v21), Deno, Bun
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
* @see https://caniuse.com/mdn-api_websocket
* @see https://nodejs.org/api/globals.html#websocket
*/
export class WS extends BaseWS {
createSocket(
uri: string,
protocols: string | string[] | undefined,
opts: Record<string, any>
) {
return !isReactNative
? protocols
? new WebSocketCtor(uri, protocols)
: new WebSocketCtor(uri)
: new WebSocketCtor(uri, protocols, opts);
}
doWrite(_packet: Packet, data: RawData) {
this.ws.send(data);
}
}

View File

@@ -0,0 +1,111 @@
import { Transport } from "../transport.js";
import { nextTick } from "../globals.node.js";
import {
Packet,
createPacketDecoderStream,
createPacketEncoderStream,
} from "engine.io-parser";
import debugModule from "debug"; // debug()
const debug = debugModule("engine.io-client:webtransport"); // debug()
/**
* WebTransport transport based on the built-in `WebTransport` object.
*
* Usage: browser, Node.js (with the `@fails-components/webtransport` package)
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebTransport
* @see https://caniuse.com/webtransport
*/
export class WT extends Transport {
private _transport: any;
private _writer: any;
get name() {
return "webtransport";
}
protected doOpen() {
try {
// @ts-ignore
this._transport = new WebTransport(
this.createUri("https"),
this.opts.transportOptions[this.name]
);
} catch (err) {
return this.emitReserved("error", err);
}
this._transport.closed
.then(() => {
debug("transport closed gracefully");
this.onClose();
})
.catch((err) => {
debug("transport closed due to %s", err);
this.onError("webtransport error", err);
});
// note: we could have used async/await, but that would require some additional polyfills
this._transport.ready.then(() => {
this._transport.createBidirectionalStream().then((stream) => {
const decoderStream = createPacketDecoderStream(
Number.MAX_SAFE_INTEGER,
this.socket.binaryType
);
const reader = stream.readable.pipeThrough(decoderStream).getReader();
const encoderStream = createPacketEncoderStream();
encoderStream.readable.pipeTo(stream.writable);
this._writer = encoderStream.writable.getWriter();
const read = () => {
reader
.read()
.then(({ done, value }) => {
if (done) {
debug("session is closed");
return;
}
debug("received chunk: %o", value);
this.onPacket(value);
read();
})
.catch((err) => {
debug("an error occurred while reading: %s", err);
});
};
read();
const packet: Packet = { type: "open" };
if (this.query.sid) {
packet.data = `{"sid":"${this.query.sid}"}`;
}
this._writer.write(packet).then(() => this.onOpen());
});
});
}
protected write(packets: Packet[]) {
this.writable = false;
for (let i = 0; i < packets.length; i++) {
const packet = packets[i];
const lastPacket = i === packets.length - 1;
this._writer.write(packet).then(() => {
if (lastPacket) {
nextTick(() => {
this.writable = true;
this.emitReserved("drain");
}, this.setTimeoutFn);
}
});
}
}
protected doClose() {
this._transport?.close();
}
}

View File

@@ -0,0 +1,65 @@
import { globalThisShim as globalThis } from "./globals.node.js";
export function pick(obj, ...attr) {
return attr.reduce((acc, k) => {
if (obj.hasOwnProperty(k)) {
acc[k] = obj[k];
}
return acc;
}, {});
}
// Keep a reference to the real timeout functions so they can be used when overridden
const NATIVE_SET_TIMEOUT = globalThis.setTimeout;
const NATIVE_CLEAR_TIMEOUT = globalThis.clearTimeout;
export function installTimerFunctions(obj, opts) {
if (opts.useNativeTimers) {
obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThis);
obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThis);
} else {
obj.setTimeoutFn = globalThis.setTimeout.bind(globalThis);
obj.clearTimeoutFn = globalThis.clearTimeout.bind(globalThis);
}
}
// base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64)
const BASE64_OVERHEAD = 1.33;
// we could also have used `new Blob([obj]).size`, but it isn't supported in IE9
export function byteLength(obj) {
if (typeof obj === "string") {
return utf8Length(obj);
}
// arraybuffer or blob
return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD);
}
function utf8Length(str) {
let c = 0,
length = 0;
for (let i = 0, l = str.length; i < l; i++) {
c = str.charCodeAt(i);
if (c < 0x80) {
length += 1;
} else if (c < 0x800) {
length += 2;
} else if (c < 0xd800 || c >= 0xe000) {
length += 3;
} else {
i++;
length += 4;
}
}
return length;
}
/**
* Generates a random 8-characters string.
*/
export function randomString() {
return (
Date.now().toString(36).substring(3) +
Math.random().toString(36).substring(2, 5)
);
}

26016
packages/engine.io-client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
{
"name": "engine.io-client",
"description": "Client for the realtime Engine",
"license": "MIT",
"version": "6.6.0",
"main": "./build/cjs/index.js",
"module": "./build/esm/index.js",
"exports": {
"./package.json": "./package.json",
"./dist/engine.io.esm.min.js": "./dist/engine.io.esm.min.js",
"./dist/engine.io.js": "./dist/engine.io.js",
"./dist/engine.io.min.js": "./dist/engine.io.min.js",
".": {
"import": {
"types": "./build/esm/index.d.ts",
"node": "./build/esm-debug/index.js",
"default": "./build/esm/index.js"
},
"require": {
"types": "./build/cjs/index.d.ts",
"default": "./build/cjs/index.js"
}
},
"./debug": {
"import": {
"types": "./build/esm/index.d.ts",
"default": "./build/esm-debug/index.js"
},
"require": {
"types": "./build/cjs/index.d.ts",
"default": "./build/cjs/index.js"
}
}
},
"types": "build/esm/index.d.ts",
"homepage": "https://github.com/socketio/engine.io-client",
"contributors": [
{
"name": "Guillermo Rauch",
"email": "rauchg@gmail.com"
},
{
"name": "Vladimir Dronnikov",
"email": "dronnikov@gmail.com"
},
{
"name": "Christoph Dorn",
"web": "https://github.com/cadorn"
},
{
"name": "Mark Mokryn",
"email": "mokesmokes@gmail.com"
}
],
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/plugin-transform-object-assign": "^7.12.1",
"@babel/preset-env": "^7.12.7",
"@fails-components/webtransport": "^0.1.7",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.0",
"@rollup/plugin-node-resolve": "^13.0.5",
"@sinonjs/fake-timers": "^7.1.2",
"@types/debug": "^4.1.12",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.1",
"@types/sinonjs__fake-timers": "^6.0.3",
"babel-loader": "^8.2.2",
"blob": "0.0.5",
"engine.io": "^6.5.2-alpha.1",
"expect.js": "^0.3.1",
"express": "^4.17.1",
"mocha": "^10.2.0",
"node-forge": "^1.3.1",
"prettier": "^2.8.1",
"rollup": "^2.58.0",
"rollup-plugin-terser": "^7.0.2",
"socket.io-browsers": "~1.0.4",
"typescript": "^4.9.5",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-remove-debug": "^0.1.0",
"zuul": "~3.11.1",
"zuul-builder-webpack": "^1.2.0",
"zuul-ngrok": "4.0.0"
},
"scripts": {
"compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
"test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
"test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs",
"test:node-fetch": "USE_FETCH=1 npm run test:node",
"test:browser": "zuul test/index.js",
"build": "rimraf ./dist && rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js",
"bundle-size": "node support/bundle-size.js",
"format:check": "prettier --check 'lib/**/*.ts' 'test/**/*.js' 'test/webtransport.mjs' 'support/**/*.js'",
"format:fix": "prettier --write 'lib/**/*.ts' 'test/**/*.js' 'test/webtransport.mjs' 'support/**/*.js'",
"prepack": "npm run compile"
},
"browser": {
"./test/node.js": false,
"./build/esm/transports/polling-xhr.node.js": "./build/esm/transports/polling-xhr.js",
"./build/esm/transports/websocket.node.js": "./build/esm/transports/websocket.js",
"./build/esm/globals.node.js": "./build/esm/globals.js",
"./build/cjs/transports/polling-xhr.node.js": "./build/cjs/transports/polling-xhr.js",
"./build/cjs/transports/websocket.node.js": "./build/cjs/transports/websocket.js",
"./build/cjs/globals.node.js": "./build/cjs/globals.js"
},
"repository": {
"type": "git",
"url": "https://github.com/socketio/engine.io-client.git"
},
"files": [
"build/",
"dist/"
]
}

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
cp ./support/package.cjs.json ./build/cjs/package.json
cp ./support/package.esm.json ./build/esm/package.json
cp -r ./build/esm/ ./build/esm-debug/
if [ "${OSTYPE:0:6}" = darwin ]; then
sed -i '' -e '/debug(/d' ./build/esm/*.js ./build/esm/**/*.js
else
sed -i -e '/debug(/d' ./build/esm/*.js ./build/esm/**/*.js
fi

View File

@@ -0,0 +1,35 @@
const { resolve } = require("node:path");
const { readFile } = require("node:fs/promises");
const { gzipSync, brotliCompressSync } = require("node:zlib");
const bundles = [
{
name: "UMD bundle",
path: "dist/engine.io.min.js",
},
{
name: "ESM bundle",
path: "dist/engine.io.esm.min.js",
},
];
function format(size) {
return (size / 1024).toFixed(1);
}
async function main() {
for (const bundle of bundles) {
const path = resolve(bundle.path);
const content = await readFile(path);
const gzip = gzipSync(content);
const brotli = brotliCompressSync(content);
console.log(`${bundle.name}`);
console.log(`min: ${format(content.length)} KB`);
console.log(`min+gzip: ${format(gzip.length)} KB`);
console.log(`min+br: ${format(brotli.length)} KB`);
console.log();
}
}
main();

View File

@@ -0,0 +1,10 @@
{
"name": "engine.io-client",
"type": "commonjs",
"browser": {
"ws": false,
"./transports/polling-xhr.node.js": "./transports/polling-xhr.js",
"./transports/websocket.node.js": "./transports/websocket.js",
"./globals.node.js": "./globals.js"
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "engine.io-client",
"type": "module",
"browser": {
"ws": false,
"./transports/polling-xhr.node.js": "./transports/polling-xhr.js",
"./transports/websocket.node.js": "./transports/websocket.js",
"./globals.node.js": "./globals.js"
}
}

View File

@@ -0,0 +1,19 @@
const config = require("./webpack.config");
module.exports = {
...config,
output: {
...config.output,
filename: "engine.io.min.js",
},
mode: "production",
module: {
rules: [
...config.module.rules,
{
test: /\.js$/,
loader: "webpack-remove-debug",
},
],
},
};

View File

@@ -0,0 +1,33 @@
const { nodeResolve } = require("@rollup/plugin-node-resolve");
const { terser } = require("rollup-plugin-terser");
const version = require("../package.json").version;
const banner = `/*!
* Engine.IO v${version}
* (c) 2014-${new Date().getFullYear()} Guillermo Rauch
* Released under the MIT License.
*/`;
module.exports = {
input: "./build/esm/index.js",
output: {
file: "./dist/engine.io.esm.min.js",
format: "esm",
sourcemap: true,
plugins: [
terser({
mangle: {
properties: {
regex: /^_/,
},
},
}),
],
banner,
},
plugins: [
nodeResolve({
browser: true,
}),
],
};

View File

@@ -0,0 +1,80 @@
const { nodeResolve } = require("@rollup/plugin-node-resolve");
const { babel } = require("@rollup/plugin-babel");
const { terser } = require("rollup-plugin-terser");
const commonjs = require("@rollup/plugin-commonjs");
const version = require("../package.json").version;
const banner = `/*!
* Engine.IO v${version}
* (c) 2014-${new Date().getFullYear()} Guillermo Rauch
* Released under the MIT License.
*/`;
module.exports = [
{
input: "./build/esm-debug/browser-entrypoint.js",
output: {
file: "./dist/engine.io.js",
format: "umd",
name: "eio",
sourcemap: true,
banner,
},
plugins: [
nodeResolve({
browser: true,
}),
commonjs(),
babel({
babelHelpers: "bundled",
presets: [["@babel/env"]],
plugins: [
"@babel/plugin-transform-object-assign",
[
"@babel/plugin-transform-classes",
{
loose: true,
},
],
],
}),
],
},
{
input: "./build/esm/browser-entrypoint.js",
output: {
file: "./dist/engine.io.min.js",
format: "umd",
name: "eio",
sourcemap: true,
plugins: [
terser({
mangle: {
properties: {
regex: /^_/,
},
},
}),
],
banner,
},
plugins: [
nodeResolve({
browser: true,
}),
babel({
babelHelpers: "bundled",
presets: [["@babel/env"]],
plugins: [
"@babel/plugin-transform-object-assign",
[
"@babel/plugin-transform-classes",
{
loose: true,
},
],
],
}),
],
},
];

View File

@@ -0,0 +1,33 @@
const { BannerPlugin } = require("webpack");
const version = require("../package.json").version;
const banner = `Engine.IO v${version}
(c) 2014-${new Date().getFullYear()} Guillermo Rauch
Released under the MIT License.`;
module.exports = {
entry: "./build/esm/index.js",
output: {
filename: "engine.io.js",
library: "eio",
libraryTarget: "umd",
globalObject: "self",
},
mode: "development",
node: false,
module: {
rules: [
{
test: /\.m?js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-object-assign"],
},
},
},
],
},
plugins: [new BannerPlugin(banner)],
};

View File

@@ -0,0 +1,6 @@
const env = require("../support/env");
require("./polling.js");
if (env.wsSupport && !env.isOldSimulator && !env.isAndroid && !env.isIE11) {
require("./ws.js");
}

View File

@@ -0,0 +1,96 @@
const expect = require("expect.js");
const { Socket } = require("../../");
const { repeat } = require("../util");
describe("arraybuffer", function () {
this.timeout(30000);
it("should be able to receive binary data when bouncing it back (polling)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new Socket({ transports: ["polling"] });
socket.binaryType = "arraybuffer";
socket.on("open", () => {
socket.send(binaryData);
socket.on("message", (data) => {
if (data === "hi") return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
it("should be able to receive binary data and a multibyte utf-8 string (polling)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
let msg = 0;
const socket = new Socket({ transports: ["polling"] });
socket.binaryType = "arraybuffer";
socket.on("open", () => {
socket.send(binaryData);
socket.send("cash money €€€");
socket.on("message", (data) => {
if (data === "hi") return;
if (msg === 0) {
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
msg++;
} else {
expect(data).to.be("cash money €€€");
socket.close();
done();
}
});
});
});
it("should be able to receive binary data when forcing base64 (polling)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new Socket({ forceBase64: true });
socket.binaryType = "arraybuffer";
socket.on("open", () => {
socket.send(binaryData);
socket.on("message", (data) => {
if (typeof data === "string") return;
expect(data).to.be.an(ArrayBuffer);
const ia = new Int8Array(data);
expect(ia).to.eql(binaryData);
socket.close();
done();
});
});
});
it("should merge binary packets according to maxPayload value", (done) => {
const socket = new Socket({ transports: ["polling"] });
socket.on("open", () => {
socket.send(new Uint8Array(72));
socket.send(new Uint8Array(20));
socket.send(repeat("a", 20));
socket.send(new Uint8Array(20).buffer);
socket.send(new Uint8Array(72));
let count = 0;
socket.on("message", () => {
count++;
if (count === 5) {
socket.close();
done();
}
});
});
});
});

View File

@@ -0,0 +1,82 @@
const expect = require("expect.js");
const eio = require("../../");
describe("arraybuffer", function () {
this.timeout(30000);
it("should be able to receive binary data when bouncing it back (ws)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new eio.Socket();
socket.binaryType = "arraybuffer";
socket.on("open", () => {
socket.on("upgrade", () => {
socket.send(binaryData);
socket.on("message", (data) => {
if (typeof data === "string") return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
it("should be able to receive binary data and a multibyte utf-8 string (ws)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
let msg = 0;
const socket = new eio.Socket();
socket.binaryType = "arraybuffer";
socket.on("open", () => {
socket.on("upgrade", () => {
socket.send(binaryData);
socket.send("cash money €€€");
socket.on("message", (data) => {
if (data === "hi") return;
if (msg === 0) {
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
msg++;
} else {
expect(data).to.be("cash money €€€");
socket.close();
done();
}
});
});
});
});
it("should be able to receive binary data when bouncing it back and forcing base64 (ws)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new eio.Socket({ forceBase64: true });
socket.binaryType = "arraybuffer";
socket.on("open", () => {
socket.on("upgrade", () => {
socket.send(binaryData);
socket.on("message", (data) => {
if (typeof data === "string") return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
});

View File

@@ -0,0 +1,26 @@
const expect = require("expect.js");
const eio = require("../");
describe("binary fallback", function () {
this.timeout(10000);
it("should be able to receive binary data when ArrayBuffer not available (polling)", (done) => {
const socket = new eio.Socket({ forceBase64: true });
socket.on("open", () => {
socket.send("give binary");
let firstPacket = true;
socket.on("message", (data) => {
if (firstPacket) {
firstPacket = false;
return;
}
expect(data.base64).to.be(true);
expect(data.data).to.equal("AAECAwQ=");
socket.close();
done();
});
});
});
});

View File

@@ -0,0 +1,6 @@
const env = require("../support/env");
require("./polling.js");
if (env.wsSupport && !env.isOldSimulator && !env.isAndroid && !env.isIE11) {
require("./ws.js");
}

View File

@@ -0,0 +1,74 @@
const expect = require("expect.js");
const { Socket } = require("../../");
const Blob = require("blob");
const { repeat } = require("../util");
describe("blob", function () {
this.timeout(30000);
it("should be able to receive binary data as blob when bouncing it back (polling)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new Socket();
socket.binaryType = "blob";
socket.on("open", () => {
socket.send(binaryData);
socket.on("message", (data) => {
if (typeof data === "string") return;
expect(data).to.be.a(Blob);
const fr = new FileReader();
fr.onload = function () {
const ab = this.result;
const ia = new Int8Array(ab);
expect(ia).to.eql(binaryData);
socket.close();
done();
};
fr.readAsArrayBuffer(data);
});
});
});
it("should be able to send data as a blob when bouncing it back (polling)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new Socket();
socket.on("open", () => {
socket.send(new Blob([binaryData.buffer]));
socket.on("message", (data) => {
if (typeof data === "string") return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
it("should merge binary packets according to maxPayload value", (done) => {
const socket = new Socket({ transports: ["polling"] });
socket.on("open", () => {
socket.send(new Blob([new Uint8Array(72)]));
socket.send(new Blob([new Uint8Array(20)]));
socket.send(repeat("a", 20));
socket.send(new Blob([new Uint8Array(20).buffer]));
socket.send(new Blob([new Uint8Array(72)]));
let count = 0;
socket.on("message", () => {
count++;
if (count === 5) {
socket.close();
done();
}
});
});
});
});

View File

@@ -0,0 +1,72 @@
const expect = require("expect.js");
const eio = require("../../");
const Blob = require("blob");
describe("blob", function () {
this.timeout(30000);
it("should be able to receive binary data as blob when bouncing it back (ws)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new eio.Socket();
socket.binaryType = "blob";
socket.on("open", () => {
socket.on("upgrade", () => {
socket.send(binaryData);
socket.on("message", (data) => {
expect(data).to.be.a(Blob);
const fr = new FileReader();
fr.onload = function () {
const ab = this.result;
const ia = new Int8Array(ab);
expect(ia).to.eql(binaryData);
socket.close();
done();
};
fr.readAsArrayBuffer(data);
});
});
});
});
it("should be able to send data as a blob when bouncing it back (ws)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new eio.Socket();
socket.on("open", () => {
socket.on("upgrade", () => {
socket.send(new Blob([binaryData.buffer]));
socket.on("message", (data) => {
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
it("should be able to send data as a blob encoded into base64 when bouncing it back (ws)", (done) => {
const binaryData = new Int8Array(5);
for (let i = 0; i < 5; i++) {
binaryData[i] = i;
}
const socket = new eio.Socket({ forceBase64: true });
socket.on("open", () => {
socket.on("upgrade", () => {
socket.send(new Blob([binaryData.buffer]));
socket.on("message", (data) => {
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
});

View File

@@ -0,0 +1,231 @@
const expect = require("expect.js");
const Socket = require("../").Socket;
const env = require("./support/env");
const { repeat } = require("./util");
describe("connection", function () {
this.timeout(20000);
it("should connect to localhost", (done) => {
const socket = new Socket();
socket.on("open", () => {
socket.on("message", (data) => {
expect(data).to.equal("hi");
socket.close();
done();
});
});
});
it("should receive multibyte utf-8 strings with polling", (done) => {
const socket = new Socket();
socket.on("open", () => {
socket.send("cash money €€€");
socket.on("message", (data) => {
if ("hi" === data) return;
expect(data).to.be("cash money €€€");
socket.close();
done();
});
});
});
it("should receive emoji", (done) => {
const socket = new Socket();
socket.on("open", () => {
socket.send(
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"
);
socket.on("message", (data) => {
if ("hi" === data) return;
expect(data).to.be(
"\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"
);
socket.close();
done();
});
});
});
it("should not send packets if socket closes", (done) => {
const socket = new Socket();
socket.on("open", () => {
let noPacket = true;
socket.on("packetCreate", () => {
noPacket = false;
});
socket.close();
socket.send("hi");
setTimeout(() => {
expect(noPacket).to.be(true);
done();
}, 1200);
});
});
it("should merge packets according to maxPayload value", (done) => {
const socket = new Socket({ transports: ["polling"] });
socket.on("open", () => {
socket.send(repeat("a", 99));
socket.send(repeat("b", 30));
socket.send(repeat("c", 30));
socket.send(repeat("d", 35)); // 3 * 1 (packet type) + 2 * 1 (separator) + 30 + 30 + 35 = 100
socket.send(repeat("€", 33));
socket.send(repeat("f", 99));
let count = 0;
socket.on("message", () => {
count++;
if (count === 6) {
socket.close();
done();
}
});
});
});
it("should send a packet whose length is above the maxPayload value anyway", (done) => {
const socket = new Socket({ transports: ["polling"] });
socket.on("open", () => {
socket.send(repeat("a", 101));
socket.send("b");
socket.on("close", () => {
done();
});
});
});
// no `Worker` on old IE
if (typeof Worker !== "undefined") {
it("should work in a worker", (done) => {
const worker = new Worker("/test/support/worker.js");
let msg = 0;
const utf8yay = "пойду спать всем спокойной ночи";
worker.onmessage = (e) => {
msg++;
if (msg === 1) {
expect(e.data).to.be("hi");
} else if (msg < 11) {
expect(e.data).to.be(utf8yay);
} else if (msg < 20) {
testBinary(e.data);
} else {
testBinary(e.data);
done();
}
};
function testBinary(data) {
const byteArray = new Uint8Array(data);
for (let i = 0; i < byteArray.byteLength; i++) {
expect(byteArray[i]).to.be(i);
}
}
});
}
if (env.wsSupport && !env.isOldSimulator && !env.isAndroid && !env.isIE11) {
it("should defer close when upgrading", (done) => {
const socket = new Socket();
socket.on("open", () => {
let upgraded = false;
socket
.on("upgrade", () => {
upgraded = true;
})
.on("upgrading", () => {
socket.on("close", () => {
expect(upgraded).to.be(true);
done();
});
socket.close();
});
});
});
it("should close on upgradeError if closing is deferred", (done) => {
const socket = new Socket();
socket.on("open", () => {
let upgradeError = false;
socket
.on("upgradeError", () => {
upgradeError = true;
})
.on("upgrading", () => {
socket.on("close", () => {
expect(upgradeError).to.be(true);
done();
});
socket.close();
socket.transport.onError("upgrade error");
});
});
});
it("should not send packets if closing is deferred", (done) => {
const socket = new Socket();
socket.on("open", () => {
let noPacket = true;
socket.on("upgrading", () => {
socket.on("packetCreate", () => {
noPacket = false;
});
socket.close();
socket.send("hi");
});
setTimeout(() => {
expect(noPacket).to.be(true);
done();
}, 1200);
});
});
it("should send all buffered packets if closing is deferred", (done) => {
const socket = new Socket();
socket.on("open", () => {
socket
.on("upgrading", () => {
socket.send("hi");
socket.close();
})
.on("close", () => {
expect(socket.writeBuffer).to.have.length(0);
done();
});
});
});
}
if (env.browser && typeof addEventListener === "function") {
it("should close the socket when receiving a beforeunload event", (done) => {
const socket = new Socket({
closeOnBeforeunload: true,
});
const createEvent = (name) => {
if (typeof Event === "function") {
return new Event(name);
} else {
// polyfill for IE
const event = document.createEvent("Event");
event.initEvent(name, true, true);
return event;
}
};
socket.on("open", () => {
const handler = () => {
expect(socket.transport.readyState).to.eql("closed");
expect(() => socket.write("ignored")).to.not.throwException();
removeEventListener("beforeunload", handler, false);
done();
};
addEventListener("beforeunload", handler, false);
dispatchEvent(createEvent("beforeunload"));
});
});
}
});

View File

@@ -0,0 +1,113 @@
const expect = require("expect.js");
const { Socket, protocol } = require("..");
const { randomString } = require("../build/cjs/util.js");
const expectedPort =
typeof location !== "undefined" && "https:" === location.protocol
? "443"
: "80";
describe("engine.io-client", () => {
let open;
before(() => {
open = Socket.prototype.open;
// override Socket#open to not connect
Socket.prototype.open = () => {};
});
after(() => {
Socket.prototype.open = open;
});
it("should expose protocol number", () => {
expect(protocol).to.be.a("number");
});
it("should properly parse http uri without port", () => {
const client = new Socket("http://localhost");
expect(client.port).to.be("80");
});
it("should properly parse https uri without port", () => {
const client = new Socket("https://localhost");
expect(client.hostname).to.be("localhost");
expect(client.port).to.be("443");
});
it("should properly parse wss uri without port", () => {
const client = new Socket("wss://localhost");
expect(client.hostname).to.be("localhost");
expect(client.port).to.be("443");
});
it("should properly parse wss uri with port", () => {
const client = new Socket("wss://localhost:2020");
expect(client.hostname).to.be("localhost");
expect(client.port).to.be("2020");
});
it("should properly parse a host without port", () => {
const client = new Socket({ host: "localhost" });
expect(client.hostname).to.be("localhost");
expect(client.port).to.be(expectedPort);
});
it("should properly parse a host with port", () => {
const client = new Socket({ host: "localhost", port: "8080" });
expect(client.hostname).to.be("localhost");
expect(client.port).to.be("8080");
});
it("should properly handle the addTrailingSlash option", () => {
const client = new Socket({ host: "localhost", addTrailingSlash: false });
expect(client.hostname).to.be("localhost");
expect(client.opts.path).to.be("/engine.io");
});
it("should properly parse an IPv6 uri without port", () => {
const client = new Socket("http://[::1]");
expect(client.hostname).to.be("::1");
expect(client.port).to.be("80");
});
it("should properly parse an IPv6 uri with port", () => {
const client = new Socket("http://[::1]:8080");
expect(client.hostname).to.be("::1");
expect(client.port).to.be("8080");
});
it("should properly parse an IPv6 host without port (1/2)", () => {
const client = new Socket({ host: "[::1]" });
expect(client.hostname).to.be("::1");
expect(client.port).to.be(expectedPort);
});
it("should properly parse an IPv6 host without port (2/2)", () => {
const client = new Socket({ secure: true, host: "[::1]" });
expect(client.hostname).to.be("::1");
expect(client.port).to.be("443");
});
it("should properly parse an IPv6 host with port", () => {
const client = new Socket({ host: "[::1]", port: "8080" });
expect(client.hostname).to.be("::1");
expect(client.port).to.be("8080");
});
it("should properly parse an IPv6 host without brace", () => {
const client = new Socket({ host: "::1" });
expect(client.hostname).to.be("::1");
expect(client.port).to.be(expectedPort);
});
it("should generate a random string", () => {
const a = randomString();
const b = randomString();
const c = randomString();
expect(a.length).to.eql(8);
expect(a).to.not.equal(b);
expect(b).to.not.equal(c);
});
});

View File

@@ -0,0 +1,8 @@
const { Socket } = require("../..");
const socket = new Socket("http://localhost:3000", {
autoUnref: false,
});
setTimeout(() => {
console.log("process should not exit");
}, 500);

View File

@@ -0,0 +1,13 @@
const { Socket } = require("../..");
const socket = new Socket("http://localhost:3000", {
autoUnref: true,
transports: ["polling"],
});
socket.on("open", () => {
console.log("open");
});
setTimeout(() => {
console.log("process should exit now");
}, 500);

View File

@@ -0,0 +1,13 @@
const { Socket } = require("../..");
const socket = new Socket("http://localhost:3000", {
autoUnref: true,
transports: ["websocket"],
});
socket.on("open", () => {
console.log("open");
});
setTimeout(() => {
console.log("process should exit now");
}, 500);

View File

@@ -0,0 +1,12 @@
const { Socket } = require("../..");
const socket = new Socket("http://localhost:3000", {
autoUnref: true,
});
socket.on("open", () => {
console.log("open");
});
setTimeout(() => {
console.log("process should exit now");
}, 500);

View File

@@ -0,0 +1,28 @@
const env = require("./support/env");
// whitelist some globals to avoid warnings
if (env.browser) {
window.___eio = null;
} else {
require("./node");
}
const Blob = require("blob");
require("./engine.io-client");
require("./socket");
require("./transport");
require("./connection");
require("./xmlhttprequest");
require("./parseuri");
if (typeof ArrayBuffer !== "undefined") {
require("./arraybuffer");
} else {
require("./binary-fallback");
}
// Blob is available in Node.js since v18, but not yet supported by the `engine.io-parser` package
if (Blob && env.browser) {
require("./blob");
}

View File

@@ -0,0 +1,138 @@
const path = require("path");
const { exec } = require("child_process");
const { Socket } = require("../");
const { repeat } = require("./util");
const expect = require("expect.js");
const { parse } = require("../build/cjs/globals.node.js");
describe("node.js", () => {
describe("autoRef option", () => {
const fixture = (filename) =>
process.execPath + " " + path.join(__dirname, "fixtures", filename);
it("should stop once the timer is triggered", (done) => {
exec(fixture("unref.js"), done);
});
it("should stop once the timer is triggered (polling)", (done) => {
exec(fixture("unref-polling-only.js"), done);
});
it("should stop once the timer is triggered (websocket)", (done) => {
exec(fixture("unref-websocket-only.js"), done);
});
it("should not stop with autoUnref set to false", (done) => {
let isComplete = false;
const process = exec(fixture("no-unref.js"), () => {
if (!isComplete) {
done(new Error("should not happen"));
}
});
setTimeout(() => {
isComplete = true;
process.kill();
done();
}, 1000);
});
});
it("should merge binary packets according to maxPayload value", (done) => {
const socket = new Socket({ transports: ["polling"] });
socket.on("open", () => {
socket.send(Buffer.allocUnsafe(72));
socket.send(Buffer.allocUnsafe(20));
socket.send(repeat("a", 20));
socket.send(Buffer.allocUnsafe(20));
socket.send(Buffer.allocUnsafe(72));
let count = 0;
socket.on("message", () => {
count++;
if (count === 5) {
socket.close();
done();
}
});
});
});
it("should send cookies with withCredentials: true", (done) => {
const socket = new Socket("http://localhost:3000", {
transports: ["polling"],
withCredentials: true,
});
socket.on("open", () => {
setTimeout(() => {
socket.send("sendHeaders");
}, 10);
});
socket.on("message", (data) => {
if (data === "hi") {
return;
}
const headers = JSON.parse(data);
expect(headers.cookie).to.eql("1=1; 2=2");
socket.close();
done();
});
});
it("should not send cookies with withCredentials: false", (done) => {
const socket = new Socket("http://localhost:3000", {
transports: ["polling"],
withCredentials: false,
});
socket.on("open", () => {
socket.send("sendHeaders");
});
socket.on("message", (data) => {
if (data === "hi") {
return;
}
const headers = JSON.parse(data);
expect(headers.cookie).to.eql(undefined);
socket.close();
done();
});
});
});
describe("cookie parsing", () => {
it("should parse a simple set-cookie header", () => {
const cookieStr = "foo=bar";
expect(parse(cookieStr)).to.eql({
name: "foo",
value: "bar",
});
});
it("should parse a complex set-cookie header", () => {
const cookieStr =
"foo=bar; Max-Age=1000; Domain=.example.com; Path=/; Expires=Tue, 01 Jul 2025 10:01:11 GMT; HttpOnly; Secure; SameSite=strict";
expect(parse(cookieStr)).to.eql({
name: "foo",
value: "bar",
expires: new Date("Tue Jul 01 2025 06:01:11 GMT-0400 (EDT)"),
});
});
it("should parse a weird but valid cookie", () => {
const cookieStr =
"foo=bar=bar&foo=foo&John=Doe&Doe=John; Domain=.example.com; Path=/; HttpOnly; Secure";
expect(parse(cookieStr)).to.eql({
name: "foo",
value: "bar=bar&foo=foo&John=Doe&Doe=John",
});
});
});

View File

@@ -0,0 +1,71 @@
// imported from https://github.com/galkn/parseuri
const expect = require("expect.js");
const parseuri = require("..").parse;
const { repeat } = require("./util");
describe("parseuri", function () {
it("should parse an uri", function () {
const http = parseuri("http://google.com"),
https = parseuri("https://www.google.com:80"),
query = parseuri("google.com:8080/foo/bar?foo=bar"),
localhost = parseuri("localhost:8080"),
ipv6 = parseuri("2001:0db8:85a3:0042:1000:8a2e:0370:7334"),
ipv6short = parseuri("2001:db8:85a3:42:1000:8a2e:370:7334"),
ipv6port = parseuri("2001:db8:85a3:42:1000:8a2e:370:7334:80"),
ipv6abbrev = parseuri("2001::7334:a:80"),
ipv6http = parseuri("http://[2001::7334:a]:80"),
ipv6query = parseuri("http://[2001::7334:a]:80/foo/bar?foo=bar");
expect(http.protocol).to.be("http");
expect(http.port).to.be("");
expect(http.host).to.be("google.com");
expect(https.protocol).to.be("https");
expect(https.port).to.be("80");
expect(https.host).to.be("www.google.com");
expect(query.port).to.be("8080");
expect(query.query).to.be("foo=bar");
expect(query.path).to.be("/foo/bar");
expect(query.relative).to.be("/foo/bar?foo=bar");
expect(query.queryKey.foo).to.be("bar");
expect(query.pathNames[0]).to.be("foo");
expect(query.pathNames[1]).to.be("bar");
expect(localhost.protocol).to.be("");
expect(localhost.host).to.be("localhost");
expect(localhost.port).to.be("8080");
expect(ipv6.protocol).to.be("");
expect(ipv6.host).to.be("2001:0db8:85a3:0042:1000:8a2e:0370:7334");
expect(ipv6.port).to.be("");
expect(ipv6short.protocol).to.be("");
expect(ipv6short.host).to.be("2001:db8:85a3:42:1000:8a2e:370:7334");
expect(ipv6short.port).to.be("");
expect(ipv6port.protocol).to.be("");
expect(ipv6port.host).to.be("2001:db8:85a3:42:1000:8a2e:370:7334");
expect(ipv6port.port).to.be("80");
expect(ipv6abbrev.protocol).to.be("");
expect(ipv6abbrev.host).to.be("2001::7334:a:80");
expect(ipv6abbrev.port).to.be("");
expect(ipv6http.protocol).to.be("http");
expect(ipv6http.port).to.be("80");
expect(ipv6http.host).to.be("2001::7334:a");
expect(ipv6query.protocol).to.be("http");
expect(ipv6query.port).to.be("80");
expect(ipv6query.host).to.be("2001::7334:a");
expect(ipv6query.relative).to.be("/foo/bar?foo=bar");
const withUserInfo = parseuri("ws://foo:bar@google.com");
expect(withUserInfo.protocol).to.eql("ws");
expect(withUserInfo.userInfo).to.eql("foo:bar");
expect(withUserInfo.user).to.eql("foo");
expect(withUserInfo.password).to.eql("bar");
expect(withUserInfo.host).to.eql("google.com");
const relativeWithQuery = parseuri("/foo?bar=@example.com");
expect(relativeWithQuery.host).to.be("");
expect(relativeWithQuery.path).to.be("/foo");
expect(relativeWithQuery.query).to.be("bar=@example.com");
expect(() => parseuri(repeat("a", 8001))).to.throwError("URI too long");
});
});

View File

@@ -0,0 +1,273 @@
const expect = require("expect.js");
const { Socket, NodeXHR, NodeWebSocket } = require("../");
const {
isIE11,
isAndroid,
isEdge,
isIPad,
useFetch,
} = require("./support/env");
const FakeTimers = require("@sinonjs/fake-timers");
const { repeat } = require("./util");
describe("Socket", function () {
this.timeout(10000);
describe("filterUpgrades", () => {
it("should return only available transports", () => {
const socket = new Socket({ transports: ["polling"] });
expect(socket._filterUpgrades(["polling", "websocket"])).to.eql([
"polling",
]);
socket.close();
});
});
it("throws an error when no transports are available", (done) => {
const socket = new Socket({ transports: [] });
let errorMessage = "";
socket.on("error", (error) => {
errorMessage = error;
});
setTimeout(() => {
expect(errorMessage).to.be("No transports available");
socket.close();
done();
});
});
it("should connect with the 2nd transport if tryAllTransports is `true` (polling)", (done) => {
const socket = new Socket({
transports: ["websocket", "polling"],
transportOptions: {
websocket: {
query: {
deny: 1,
},
},
},
tryAllTransports: true,
});
socket.on("open", () => {
expect(socket.transport.name).to.eql("polling");
socket.close();
done();
});
});
it("should connect with the 2nd transport if tryAllTransports is `true` (websocket)", (done) => {
const socket = new Socket({
transports: ["polling", "websocket"],
transportOptions: {
polling: {
query: {
deny: 1,
},
},
},
tryAllTransports: true,
});
socket.on("open", () => {
expect(socket.transport.name).to.eql("websocket");
socket.close();
done();
});
});
it("should not connect with the 2nd transport if tryAllTransports is `false`", (done) => {
const socket = new Socket({
transports: ["polling", "websocket"],
transportOptions: {
polling: {
query: {
deny: 1,
},
},
},
});
socket.on("error", (err) => {
expect(err.message).to.eql(
useFetch ? "fetch read error" : "xhr poll error"
);
done();
});
});
it("should connect with a custom transport implementation (polling)", (done) => {
const socket = new Socket({
transports: [NodeXHR],
});
socket.on("open", () => {
expect(socket.transport.name).to.eql("polling");
socket.close();
done();
});
});
it("should connect with a custom transport implementation (websocket)", (done) => {
const socket = new Socket({
transports: [NodeWebSocket],
});
socket.on("open", () => {
expect(socket.transport.name).to.eql("websocket");
socket.close();
done();
});
});
describe("fake timers", function () {
before(function () {
if (isIE11 || isAndroid || isEdge || isIPad) {
this.skip();
}
});
it("uses window timeout by default", (done) => {
const clock = FakeTimers.install();
const socket = new Socket({ transports: [] });
let errorMessage = "";
socket.on("error", (error) => {
errorMessage = error;
});
clock.tick(1); // Should trigger error emit.
expect(errorMessage).to.be("No transports available");
clock.uninstall();
socket.close();
done();
});
it.skip("uses custom timeout when provided", (done) => {
const clock = FakeTimers.install();
const socket = new Socket({
transports: [],
useNativeTimers: true,
});
let errorMessage = "";
socket.on("error", (error) => {
errorMessage = error;
});
socket.open();
// Socket should not use the mocked clock, so this should have no side
// effects.
clock.tick(1);
expect(errorMessage).to.be("");
clock.uninstall();
setTimeout(() => {
try {
expect(errorMessage).to.be("No transports available");
socket.close();
done();
} finally {
}
}, 1);
});
});
describe("close", () => {
it("provides details when maxHttpBufferSize is reached (polling)", (done) => {
const socket = new Socket({ transports: ["polling"] });
socket.on("open", () => {
socket.send(repeat("a", 101)); // over the maxHttpBufferSize value of the server
});
socket.on("error", (err) => {
expect(err).to.be.an(Error);
expect(err.type).to.eql("TransportError");
expect(err.description).to.eql(413);
if (useFetch) {
expect(err.message).to.eql("fetch write error");
} else {
expect(err.message).to.eql("xhr post error");
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql("");
}
});
socket.on("close", (reason, details) => {
expect(reason).to.eql("transport error");
expect(details).to.be.an(Error);
done();
});
});
it("provides details when maxHttpBufferSize is reached (websocket)", (done) => {
const socket = new Socket({ transports: ["websocket"] });
socket.on("open", () => {
socket.send(repeat("a", 101)); // over the maxHttpBufferSize value of the server
});
socket.on("close", (reason, details) => {
if (isIE11) {
expect(reason).to.eql("transport error");
expect(details).to.be.an(Error);
} else {
expect(reason).to.eql("transport close");
expect(details.description).to.eql("websocket connection closed");
// details.context is a CloseEvent object
expect(details.context.code).to.eql(1009); // "Message Too Big" (see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code)
expect(details.context.reason).to.eql("");
// note: details.context.wasClean is false in the browser, but true in Node.js
}
done();
});
});
it("provides details when the session ID is unknown (polling)", (done) => {
const socket = new Socket({
transports: ["polling"],
query: { sid: "abc" },
});
socket.on("error", (err) => {
expect(err).to.be.an(Error);
expect(err.type).to.eql("TransportError");
expect(err.description).to.eql(400);
if (useFetch) {
expect(err.message).to.eql("fetch read error");
} else {
expect(err.message).to.eql("xhr poll error");
// err.context is a XMLHttpRequest object
expect(err.context.readyState).to.eql(4);
expect(err.context.responseText).to.eql(
'{"code":1,"message":"Session ID unknown"}'
);
}
});
socket.on("close", (reason, details) => {
expect(reason).to.eql("transport error");
expect(details).to.be.an(Error);
done();
});
});
it("provides details when the session ID is unknown (websocket)", (done) => {
const socket = new Socket({
transports: ["websocket"],
query: { sid: "abc" },
});
socket.on("error", (err) => {
expect(err).to.be.an(Error);
expect(err.type).to.eql("TransportError");
expect(err.message).to.eql("websocket error");
// err.description is a generic Event
expect(err.description.type).to.be("error");
});
socket.on("close", (reason, details) => {
expect(reason).to.eql("transport error");
expect(details).to.be.an(Error);
done();
});
});
});
});

View File

@@ -0,0 +1,37 @@
/* global location:true */
// WARNING this is bad practice
// we only do this in our tests because we need to test engine.io-client
// support in browsers and in node.js
// some tests do not yet work in both
exports.browser = typeof window !== "undefined";
exports.wsSupport = !!(
typeof window === "undefined" ||
window.WebSocket ||
window.MozWebSocket
);
const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
exports.isOldSimulator =
~userAgent.indexOf("iPhone OS 4") || ~userAgent.indexOf("iPhone OS 5");
exports.isIE9 = /MSIE 9/.test(userAgent);
exports.isIE10 = /MSIE 10/.test(userAgent);
exports.isIE11 = !!userAgent.match(/Trident.*rv[ :]*11\./); // ws doesn't work at all in sauce labs
exports.isAndroid = userAgent.match(/Android/i);
exports.isEdge = /Edg/.test(userAgent);
exports.isIPad = /iPad/.test(userAgent);
if (typeof location === "undefined") {
location = {
hostname: "localhost",
port: 3000,
};
}
exports.useFetch = !exports.browser && process.env.USE_FETCH !== undefined;
if (exports.useFetch) {
console.warn("testing with fetch() instead of XMLHttpRequest");
const { transports, Fetch } = require("../..");
transports.polling = Fetch;
}

View File

@@ -0,0 +1,88 @@
// this is a test server to support tests which make requests
const express = require("express");
const { join } = require("path");
const { createServer } = require("http");
const { attach } = require("engine.io");
const { rollup } = require("rollup");
const rollupConfig = require("../../support/rollup.config.umd.js")[1];
const { serialize } = require("cookie");
let httpServer, engine;
exports.mochaHooks = {
beforeAll() {
const app = express();
httpServer = createServer(app);
engine = attach(httpServer, {
pingInterval: 500,
maxHttpBufferSize: 100,
allowRequest: (req, fn) => {
const denyRequest = new URL(`http://${req.url}`).searchParams.has(
"deny"
);
fn(null, !denyRequest);
},
});
rollup(rollupConfig).then(async (bundle) => {
await bundle.write({
...rollupConfig.output,
file: "./test/support/public/engine.io.min.js",
sourcemap: false,
});
await bundle.close();
});
httpServer.listen(process.env.ZUUL_PORT || 3000);
// serve worker.js and engine.io.js as raw file
app.use("/test/support", express.static(join(__dirname, "public")));
engine.on("connection", (socket) => {
socket.send("hi");
// Bounce any received messages back
socket.on("message", (data) => {
if (data === "give binary") {
const abv = new Int8Array(5);
for (let i = 0; i < 5; i++) {
abv[i] = i;
}
socket.send(abv);
return;
} else if (data === "give utf8") {
socket.send("пойду спать всем спокойной ночи");
return;
} else if (data === "sendHeaders") {
const headers = socket.transport?.dataReq?.headers;
return socket.send(JSON.stringify(headers));
}
socket.send(data);
});
});
engine.on("initial_headers", (headers) => {
headers["set-cookie"] = [
serialize("1", "1", { maxAge: 86400 }),
serialize("2", "2", {
sameSite: true,
path: "/",
httpOnly: true,
secure: true,
}),
serialize("3", "3", { maxAge: 0 }),
serialize("4", "4", { expires: new Date() }),
];
});
},
afterAll() {
httpServer.close();
engine.close();
},
};

View File

@@ -0,0 +1,16 @@
/* global importScripts,eio,postMessage */
importScripts("/test/support/engine.io.min.js");
var socket = eio();
var count = 0;
socket.on("message", function (msg) {
count++;
if (count < 10) {
socket.send("give utf8");
} else if (count < 20) {
socket.send("give binary");
}
postMessage(msg);
});

View File

@@ -0,0 +1,49 @@
// this is a test server to support tests which make requests
const express = require("express");
const app = express();
const join = require("path").join;
const http = require("http").Server(app);
const server = require("engine.io").attach(http, {
pingInterval: 500,
maxHttpBufferSize: 100,
});
const { rollup } = require("rollup");
const rollupConfig = require("../../support/rollup.config.umd.js")[1];
rollup(rollupConfig).then(async (bundle) => {
await bundle.write({
...rollupConfig.output,
file: "./test/support/public/engine.io.min.js",
sourcemap: false,
});
await bundle.close();
});
http.listen(process.env.ZUUL_PORT || 3000);
// serve worker.js and engine.io.js as raw file
app.use("/test/support", express.static(join(__dirname, "public")));
server.on("connection", (socket) => {
socket.send("hi");
// Bounce any received messages back
socket.on("message", (data) => {
if (data === "give binary") {
const abv = new Int8Array(5);
for (let i = 0; i < 5; i++) {
abv[i] = i;
}
socket.send(abv);
return;
} else if (data === "give utf8") {
socket.send("пойду спать всем спокойной ночи");
return;
}
socket.send(data);
});
});

View File

@@ -0,0 +1,330 @@
const expect = require("expect.js");
const eio = require("../");
const env = require("./support/env");
// Disables eslint to capitalise constructor names
/* eslint-disable new-cap */
describe("Transport", () => {
describe("rememberUpgrade", () => {
it("should remember websocket connection", (done) => {
const socket = new eio.Socket();
expect(socket.transport.name).to.be("polling");
let timedout = false;
const timeout = setTimeout(() => {
timedout = true;
socket.close();
done();
}, 300);
socket.on("upgrade", (transport) => {
if (timedout) return;
clearTimeout(timeout);
socket.close();
if (transport.name === "websocket") {
const socket2 = new eio.Socket({ rememberUpgrade: true });
expect(socket2.transport.name).to.be("websocket");
}
done();
});
});
it("should not remember websocket connection", (done) => {
const socket = new eio.Socket();
expect(socket.transport.name).to.be("polling");
let timedout = false;
const timeout = setTimeout(() => {
timedout = true;
socket.close();
done();
}, 300);
socket.on("upgrade", (transport) => {
if (timedout) return;
clearTimeout(timeout);
socket.close();
if (transport.name === "websocket") {
const socket2 = new eio.Socket({ rememberUpgrade: false });
expect(socket2.transport.name).to.not.be("websocket");
}
done();
});
});
});
describe("public constructors", () => {
it("should include Transport", () => {
expect(eio.Transport).to.be.a("function");
});
it("should include Polling and WebSocket", () => {
expect(eio.transports).to.be.an("object");
expect(eio.transports.polling).to.be.a("function");
expect(eio.transports.websocket).to.be.a("function");
});
});
describe("transport uris", () => {
it("should generate an http uri", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
secure: false,
query: { sid: "test" },
timestampRequests: false,
});
expect(polling.uri()).to.contain("http://localhost/engine.io?sid=test");
});
it("should generate an http uri w/o a port", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
secure: false,
query: { sid: "test" },
port: 80,
timestampRequests: false,
});
expect(polling.uri()).to.contain("http://localhost/engine.io?sid=test");
});
it("should generate an http uri with a port", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
secure: false,
query: { sid: "test" },
port: 3000,
timestampRequests: false,
});
expect(polling.uri()).to.contain(
"http://localhost:3000/engine.io?sid=test"
);
});
it("should generate an https uri w/o a port", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
secure: true,
query: { sid: "test" },
port: 443,
timestampRequests: false,
});
expect(polling.uri()).to.contain("https://localhost/engine.io?sid=test");
});
it("should generate a timestamped uri", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
timestampParam: "t",
timestampRequests: true,
});
expect(polling.uri()).to.match(
/http:\/\/localhost\/engine\.io\?(j=[0-9]+&)?(t=[0-9A-Za-z-_]+)/
);
});
it("should generate an ipv6 uri", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "::1",
secure: false,
port: 80,
timestampRequests: false,
});
expect(polling.uri()).to.contain("http://[::1]/engine.io");
});
it("should generate an ipv6 uri with port", () => {
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "::1",
secure: false,
port: 8080,
timestampRequests: false,
});
expect(polling.uri()).to.contain("http://[::1]:8080/engine.io");
});
it("should generate a ws uri", () => {
const ws = new eio.transports.websocket({
path: "/engine.io",
hostname: "test",
secure: false,
query: { transport: "websocket" },
timestampRequests: false,
});
expect(ws.uri()).to.be("ws://test/engine.io?transport=websocket");
});
it("should generate a wss uri", () => {
const ws = new eio.transports.websocket({
path: "/engine.io",
hostname: "test",
secure: true,
query: {},
timestampRequests: false,
});
expect(ws.uri()).to.be("wss://test/engine.io");
});
it("should timestamp ws uris", () => {
const ws = new eio.transports.websocket({
path: "/engine.io",
hostname: "localhost",
timestampParam: "woot",
timestampRequests: true,
});
expect(ws.uri()).to.match(
/ws:\/\/localhost\/engine\.io\?woot=[0-9A-Za-z-_]+/
);
});
it("should generate a ws ipv6 uri", () => {
const ws = new eio.transports.websocket({
path: "/engine.io",
hostname: "::1",
secure: false,
port: 80,
timestampRequests: false,
});
expect(ws.uri()).to.be("ws://[::1]/engine.io");
});
it("should generate a ws ipv6 uri with port", () => {
const ws = new eio.transports.websocket({
path: "/engine.io",
hostname: "::1",
secure: false,
port: 8080,
timestampRequests: false,
});
expect(ws.uri()).to.be("ws://[::1]:8080/engine.io");
});
});
// these are server only
if (!env.browser) {
describe("options", () => {
it("should accept an `agent` option for WebSockets", (done) => {
const polling = new eio.transports.websocket({
path: "/engine.io",
hostname: "localhost",
agent: {
addRequest: () => {
done();
},
},
});
polling.doOpen();
});
it("should accept an `agent` option for XMLHttpRequest", function (done) {
if (env.useFetch) {
return this.skip();
}
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
agent: {
addRequest: () => {
done();
},
},
});
polling.doOpen();
});
describe("for extraHeaders", () => {
it("should correctly set them for WebSockets", () => {
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",
};
const polling = new eio.transports.websocket({
path: "/engine.io",
hostname: "localhost",
extraHeaders: headers,
});
expect(polling.opts.extraHeaders).to.equal(headers);
});
it("should correctly set them for XMLHttpRequest", () => {
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",
};
const polling = new eio.transports.polling({
path: "/engine.io",
hostname: "localhost",
extraHeaders: headers,
});
expect(polling.opts.extraHeaders).to.equal(headers);
});
});
describe("perMessageDeflate", () => {
it("should set threshold", (done) => {
const socket = new eio.Socket({
transports: ["websocket"],
perMessageDeflate: { threshold: 0 },
});
socket.on("open", () => {
const ws = socket.transport.ws;
const send = ws.send;
ws.send = (data, opts, callback) => {
ws.send = send;
ws.send(data, opts, callback);
expect(opts.compress).to.be(true);
socket.close();
done();
};
socket.send("hi", { compress: true });
});
});
it("should not compress when the byte size is below threshold", (done) => {
const socket = new eio.Socket({ transports: ["websocket"] });
socket.on("open", () => {
const ws = socket.transport.ws;
const send = ws.send;
ws.send = (data, opts, callback) => {
ws.send = send;
ws.send(data, opts, callback);
expect(opts.compress).to.be(false);
socket.close();
done();
};
socket.send("hi", { compress: true });
});
});
});
});
}
describe("options", () => {
it("should accept an `extraHeaders` option for XMLHttpRequest in browser", () => {
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",
};
const socket = new eio.Socket({
transportOptions: {
polling: {
extraHeaders: headers,
},
},
});
expect(socket.transport.name).to.be("polling");
expect(socket.transport.opts.extraHeaders).to.equal(headers);
});
});
});

View File

@@ -0,0 +1,404 @@
// imported from https://github.com/fails-components/webtransport/blob/master/test/fixtures/certificate.js
// @ts-expect-error node-forge has no types and @types/node-forge do not include oids
import forge from 'node-forge'
import { webcrypto as crypto, X509Certificate } from 'crypto'
const { pki, asn1, oids } = forge
// taken from node-forge
/**
* Converts an X.509 subject or issuer to an ASN.1 RDNSequence.
*
* @param {any} obj the subject or issuer (distinguished name).
*
* @return the ASN.1 RDNSequence.
*/
function _dnToAsn1(obj) {
// create an empty RDNSequence
const rval = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [])
// iterate over attributes
let attr, set
const attrs = obj.attributes
for (let i = 0; i < attrs.length; ++i) {
attr = attrs[i]
let value = attr.value
// reuse tag class for attribute value if available
let valueTagClass = asn1.Type.PRINTABLESTRING
if ('valueTagClass' in attr) {
valueTagClass = attr.valueTagClass
if (valueTagClass === asn1.Type.UTF8) {
value = forge.util.encodeUtf8(value)
}
// FIXME: handle more encodings
}
// create a RelativeDistinguishedName set
// each value in the set is an AttributeTypeAndValue first
// containing the type (an OID) and second the value
set = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
// AttributeType
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.OID,
false,
asn1.oidToDer(attr.type).getBytes()
),
// AttributeValue
asn1.create(asn1.Class.UNIVERSAL, valueTagClass, false, value)
])
])
rval.value.push(set)
}
return rval
}
const jan_1_1950 = new Date('1950-01-01T00:00:00Z') // eslint-disable-line camelcase
const jan_1_2050 = new Date('2050-01-01T00:00:00Z') // eslint-disable-line camelcase
// taken from node-forge almost not modified
/**
* Converts a Date object to ASN.1
* Handles the different format before and after 1st January 2050
*
* @param {Date} date date object.
*
* @return the ASN.1 object representing the date.
*/
function _dateToAsn1(date) {
// eslint-disable-next-line camelcase
if (date >= jan_1_1950 && date < jan_1_2050) {
return asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.UTCTIME,
false,
asn1.dateToUtcTime(date)
)
} else {
return asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.GENERALIZEDTIME,
false,
asn1.dateToGeneralizedTime(date)
)
}
}
// taken from node-forge almost not modified
/**
* Convert signature parameters object to ASN.1
*
* @param {string} oid Signature algorithm OID
* @param {any} params The signature parameters object
* @return ASN.1 object representing signature parameters
*/
function _signatureParametersToAsn1(oid, params) {
const parts = []
switch (oid) {
case oids['RSASSA-PSS']:
if (params.hash.algorithmOid !== undefined) {
parts.push(
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.OID,
false,
asn1.oidToDer(params.hash.algorithmOid).getBytes()
),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '')
])
])
)
}
if (params.mgf.algorithmOid !== undefined) {
parts.push(
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.OID,
false,
asn1.oidToDer(params.mgf.algorithmOid).getBytes()
),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.OID,
false,
asn1.oidToDer(params.mgf.hash.algorithmOid).getBytes()
),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '')
])
])
])
)
}
if (params.saltLength !== undefined) {
parts.push(
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.INTEGER,
false,
asn1.integerToDer(params.saltLength).getBytes()
)
])
)
}
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, parts)
default:
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.NULL, false, '')
}
}
// taken from node-forge and modified to work with ECDSA
/**
* Gets the ASN.1 TBSCertificate part of an X.509v3 certificate.
*
* @param {any} cert the certificate.
*
* @return the asn1 TBSCertificate.
*/
function getTBSCertificate(cert) {
// TBSCertificate
const notBefore = _dateToAsn1(cert.validity.notBefore)
const notAfter = _dateToAsn1(cert.validity.notAfter)
const tbs = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
// version
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
// integer
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.INTEGER,
false,
asn1.integerToDer(cert.version).getBytes()
)
]),
// serialNumber
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.INTEGER,
false,
forge.util.hexToBytes(cert.serialNumber)
),
// signature
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
// algorithm
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.OID,
false,
asn1.oidToDer(cert.siginfo.algorithmOid).getBytes()
),
// parameters
_signatureParametersToAsn1(
cert.siginfo.algorithmOid,
cert.siginfo.parameters
)
]),
// issuer
_dnToAsn1(cert.issuer),
// validity
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
notBefore,
notAfter
]),
// subject
_dnToAsn1(cert.subject),
// SubjectPublicKeyInfo
// here comes our modification, we are other objects here
asn1.fromDer(
new forge.util.ByteBuffer(
cert.publicKey
) /* is in already SPKI format but in DER encoding */
)
])
if (cert.issuer.uniqueId) {
// issuerUniqueID (optional)
tbs.value.push(
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.BITSTRING,
false,
// TODO: support arbitrary bit length ids
String.fromCharCode(0x00) + cert.issuer.uniqueId
)
])
)
}
if (cert.subject.uniqueId) {
// subjectUniqueID (optional)
tbs.value.push(
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 2, true, [
asn1.create(
asn1.Class.UNIVERSAL,
asn1.Type.BITSTRING,
false,
// TODO: support arbitrary bit length ids
String.fromCharCode(0x00) + cert.subject.uniqueId
)
])
)
}
if (cert.extensions.length > 0) {
// extensions (optional)
tbs.value.push(pki.certificateExtensionsToAsn1(cert.extensions))
}
return tbs
}
// function taken form selfsigned
// a hexString is considered negative if it's most significant bit is 1
// because serial numbers use ones' complement notation
// this RFC in section 4.1.2.2 requires serial numbers to be positive
// http://www.ietf.org/rfc/rfc5280.txt
/**
* @param {string} hexString
* @returns
*/
function toPositiveHex(hexString) {
let mostSiginficativeHexAsInt = parseInt(hexString[0], 16)
if (mostSiginficativeHexAsInt < 8) {
return hexString
}
mostSiginficativeHexAsInt -= 8
return mostSiginficativeHexAsInt.toString() + hexString.substring(1)
}
// the next is an edit of the selfsigned function reduced to the function necessary for webtransport
/**
* @typedef {object} Certificate
* @property {string} public
* @property {string} private
* @property {string} cert
* @property {Uint8Array} hash
* @property {string} fingerprint
*
* @param {*} attrs
* @param {*} options
* @returns {Promise<Certificate | null>}
*/
export async function generateWebTransportCertificate(attrs, options) {
try {
const keyPair = await crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256'
},
true,
['sign', 'verify']
)
const cert = pki.createCertificate()
cert.serialNumber = toPositiveHex(
forge.util.bytesToHex(forge.random.getBytesSync(9))
) // the serial number can be decimal or hex (if preceded by 0x)
cert.validity.notBefore = new Date()
cert.validity.notAfter = new Date()
cert.validity.notAfter.setDate(
cert.validity.notBefore.getDate() + (options.days || 14)
) // per spec only 14 days allowed
cert.setSubject(attrs)
cert.setIssuer(attrs)
const privateKey = crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
const publicKey = (cert.publicKey = await crypto.subtle.exportKey(
'spki',
keyPair.publicKey
))
cert.setExtensions(
options.extensions || [
{
name: 'basicConstraints',
cA: true
},
{
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true
},
{
name: 'subjectAltName',
altNames: [
{
type: 6, // URI
value: 'http://example.org/webid#me'
}
]
}
]
)
// to signing
// patch oids object
oids['1.2.840.10045.4.3.2'] = 'ecdsa-with-sha256'
oids['ecdsa-with-sha256'] = '1.2.840.10045.4.3.2'
cert.siginfo.algorithmOid = cert.signatureOid = '1.2.840.10045.4.3.2' // 'ecdsa-with-sha256'
cert.tbsCertificate = getTBSCertificate(cert)
const encoded = Buffer.from(
asn1.toDer(cert.tbsCertificate).getBytes(),
'binary'
)
cert.md = crypto.subtle.digest('SHA-256', encoded)
cert.signature = crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-256' }
},
keyPair.privateKey,
encoded
)
cert.md = await cert.md
cert.signature = await cert.signature
const pemcert = pki.certificateToPem(cert)
const x509cert = new X509Certificate(pemcert)
const certhash = Buffer.from(
x509cert.fingerprint256.split(':').map((el) => parseInt(el, 16))
)
const pem = {
private: forge.pem.encode({
type: 'PRIVATE KEY',
body: new forge.util.ByteBuffer(await privateKey).getBytes()
}),
public: forge.pem.encode({
type: 'PUBLIC KEY',
body: new forge.util.ByteBuffer(publicKey).getBytes()
}),
cert: pemcert,
hash: certhash,
fingerprint: x509cert.fingerprint256
}
return pem
} catch (error) {
console.log('error in generate certificate', error)
return null
}
}

View File

@@ -0,0 +1,14 @@
// polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
exports.repeat = function (str, count) {
if (String.prototype.repeat) {
return str.repeat(count);
}
const maxCount = str.length * count;
count = Math.floor(Math.log(count) / Math.log(2));
while (count) {
str += str;
count--;
}
str += str.substring(0, maxCount - str.length);
return str;
};

View File

@@ -0,0 +1,302 @@
import { Http3Server, WebTransport } from "@fails-components/webtransport";
import { Http3EventLoop } from "@fails-components/webtransport/lib/event-loop.js";
import expect from "expect.js";
import { Server } from "engine.io";
import { Socket } from "../build/esm-debug/index.js";
import { generateWebTransportCertificate } from "./util-wt.mjs";
import { createServer } from "http";
import { TransformStream } from "stream/web";
if (typeof window === "undefined") {
global.WebTransport = WebTransport;
global.TransformStream = TransformStream;
}
async function setup(opts, cb) {
const certificate = await generateWebTransportCertificate(
[{ shortName: "CN", value: "localhost" }],
{
days: 14, // the total length of the validity period MUST NOT exceed two weeks (https://w3c.github.io/webtransport/#custom-certificate-requirements)
}
);
const engine = new Server(opts);
const h3Server = new Http3Server({
port: 0,
host: "0.0.0.0",
secret: "changeit",
cert: certificate.cert,
privKey: certificate.private,
});
(async () => {
try {
const stream = await h3Server.sessionStream("/engine.io/");
const sessionReader = stream.getReader();
while (true) {
const { done, value } = await sessionReader.read();
if (done) {
break;
}
engine.onWebTransportSession(value);
}
} catch (ex) {
console.error("Server error", ex);
}
})();
h3Server.startServer();
h3Server.onServerListening = () => cb({ engine, h3Server, certificate });
}
function success(engine, h3server, done) {
engine.close();
h3server.stopServer();
done();
}
function createSocket(port, certificate, opts) {
return new Socket(
`http://127.0.0.1:${port}`,
Object.assign(
{
transportOptions: {
webtransport: {
serverCertificateHashes: [
{
algorithm: "sha-256",
value: certificate.hash,
},
],
},
},
},
opts
)
);
}
describe("WebTransport", () => {
after(() => {
Http3EventLoop.globalLoop.shutdownEventLoop(); // manually shutdown the event loop, instead of waiting 20s
});
it("should allow to connect with WebTransport directly", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
socket.on("open", () => {
success(engine, h3Server, done);
});
});
});
it("should allow to upgrade to WebTransport", (done) => {
setup(
{
transports: ["polling", "webtransport"],
},
({ engine, h3Server, certificate }) => {
const httpServer = createServer();
engine.attach(httpServer);
httpServer.listen(h3Server.port);
const socket = createSocket(h3Server.port, certificate, {
transports: ["polling", "webtransport"],
});
socket.on("upgrade", () => {
httpServer.close();
success(engine, h3Server, done);
});
}
);
});
it("should favor WebTransport over WebSocket", (done) => {
setup(
{
transports: ["polling", "websocket", "webtransport"],
},
({ engine, h3Server, certificate }) => {
const httpServer = createServer();
engine.attach(httpServer);
httpServer.listen(h3Server.port);
const socket = createSocket(h3Server.port, certificate, {
transports: ["polling", "websocket", "webtransport"],
});
socket.on("upgrade", (transport) => {
expect(transport.name).to.eql("webtransport");
httpServer.close();
success(engine, h3Server, done);
});
}
);
});
it("should send ping/pong packets", (done) => {
setup(
{
pingInterval: 20,
},
({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
let i = 0;
socket.on("heartbeat", () => {
i++;
if (i === 10) {
success(engine, h3Server, done);
}
});
}
);
});
it("should handle connections closed by the server", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
engine.on("connection", (serverSocket) => {
serverSocket.close();
});
socket.on("close", (reason) => {
expect(reason).to.eql("transport close");
success(engine, h3Server, done);
});
});
});
it("should handle connections closed by the client", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
engine.on("connection", (serverSocket) => {
serverSocket.on("close", (reason) => {
expect(reason).to.eql("transport close");
success(engine, h3Server, done);
});
});
socket.on("open", () => {
socket.close();
});
});
});
it("should send some plaintext data (client to server)", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
engine.on("connection", (serverSocket) => {
serverSocket.on("message", (data) => {
expect(data).to.eql("hello");
success(engine, h3Server, done);
});
});
socket.on("open", () => {
socket.send("hello");
});
});
});
it("should send some plaintext data (server to client)", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
engine.on("connection", (serverSocket) => {
serverSocket.send("hello");
});
socket.on("message", (data) => {
expect(data).to.eql("hello");
success(engine, h3Server, done);
});
});
});
it("should send some binary data (client to server)", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
engine.on("connection", (serverSocket) => {
serverSocket.on("message", (data) => {
expect(data).to.eql(Uint8Array.from([1, 2, 3]));
success(engine, h3Server, done);
});
});
socket.on("open", () => {
socket.send(Uint8Array.from([1, 2, 3]));
});
});
});
it("should send some binary data (server to client) (as ArrayBuffer)", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
socket.binaryType = "arraybuffer";
engine.on("connection", (serverSocket) => {
serverSocket.send(Uint8Array.from([1, 2, 3]));
});
socket.on("message", (data) => {
expect(data).to.be.an(ArrayBuffer);
expect(new Uint8Array(data)).to.eql(Uint8Array.of(1, 2, 3));
success(engine, h3Server, done);
});
});
});
it("should send some binary data (server to client) (as Buffer)", (done) => {
setup({}, ({ engine, h3Server, certificate }) => {
const socket = createSocket(h3Server.port, certificate, {
transports: ["webtransport"],
});
engine.on("connection", (serverSocket) => {
serverSocket.send(Uint8Array.from([1, 2, 3]));
});
socket.on("message", (data) => {
expect(Buffer.isBuffer(data)).to.be(true);
expect(data).to.eql(Uint8Array.of(1, 2, 3));
success(engine, h3Server, done);
});
});
});
});

View File

@@ -0,0 +1,125 @@
const expect = require("expect.js");
const { newRequest } = require("../build/cjs/transports/polling-xhr.node.js");
const env = require("./support/env");
describe("XMLHttpRequest", () => {
if (env.isIE9) {
describe("IE8_9", () => {
context("when xdomain is false", () => {
it("should have same properties as XMLHttpRequest does", () => {
const xhra = newRequest({
xdomain: false,
xscheme: false,
enablesXDR: false,
});
expect(xhra).to.be.an("object");
expect(xhra).to.have.property("open");
expect(xhra).to.have.property("onreadystatechange");
const xhrb = newRequest({
xdomain: false,
xscheme: false,
enablesXDR: true,
});
expect(xhrb).to.be.an("object");
expect(xhrb).to.have.property("open");
expect(xhrb).to.have.property("onreadystatechange");
const xhrc = newRequest({
xdomain: false,
xscheme: true,
enablesXDR: false,
});
expect(xhrc).to.be.an("object");
expect(xhrc).to.have.property("open");
expect(xhrc).to.have.property("onreadystatechange");
const xhrd = newRequest({
xdomain: false,
xscheme: true,
enablesXDR: true,
});
expect(xhrd).to.be.an("object");
expect(xhrd).to.have.property("open");
expect(xhrd).to.have.property("onreadystatechange");
});
});
context("when xdomain is true", () => {
context("when xscheme is false and enablesXDR is true", () => {
it("should have same properties as XDomainRequest does", () => {
const xhr = newRequest({
xdomain: true,
xscheme: false,
enablesXDR: true,
});
expect(xhr).to.be.an("object");
expect(xhr).to.have.property("open");
expect(xhr).to.have.property("onload");
expect(xhr).to.have.property("onerror");
});
});
context("when xscheme is true", () => {
it("should not have open in properties", () => {
const xhra = newRequest({
xdomain: true,
xscheme: true,
enablesXDR: false,
});
expect(xhra).to.be.an("object");
expect(xhra).not.to.have.property("open");
const xhrb = newRequest({
xdomain: true,
xscheme: true,
enablesXDR: true,
});
expect(xhrb).to.be.an("object");
expect(xhrb).not.to.have.property("open");
});
});
context("when enablesXDR is false", () => {
it("should not have open in properties", () => {
const xhra = newRequest({
xdomain: true,
xscheme: false,
enablesXDR: false,
});
expect(xhra).to.be.an("object");
expect(xhra).not.to.have.property("open");
const xhrb = newRequest({
xdomain: true,
xscheme: true,
enablesXDR: false,
});
expect(xhrb).to.be.an("object");
expect(xhrb).not.to.have.property("open");
});
});
});
});
}
if (env.isIE10 || env.isIE11) {
describe("IE10_11", () => {
context("when enablesXDR is true and xscheme is false", () => {
it("should have same properties as XMLHttpRequest does", () => {
const xhra = newRequest({
xdomain: false,
xscheme: false,
enablesXDR: true,
});
expect(xhra).to.be.an("object");
expect(xhra).to.have.property("open");
expect(xhra).to.have.property("onreadystatechange");
const xhrb = newRequest({
xdomain: true,
xscheme: false,
enablesXDR: true,
});
expect(xhrb).to.be.an("object");
expect(xhrb).to.have.property("open");
expect(xhrb).to.have.property("onreadystatechange");
});
});
});
}
});

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"outDir": "build/esm/",
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"declaration": true
},
"include": [
"./lib/**/*"
]
}

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"outDir": "build/cjs/",
"target": "es2018", // Node.js 10 (https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping)
"module": "commonjs",
"declaration": true,
"esModuleInterop": true
},
"include": [
"./lib/**/*"
]
}

View File

@@ -0,0 +1,56 @@
'use strict';
const browsers = require('socket.io-browsers');
const zuulConfig = module.exports = {
ui: 'mocha-bdd',
// test on localhost by default
local: true,
open: true,
concurrency: 2, // ngrok only accepts two tunnels by default
// if browser does not sends output in 120s since last output:
// stop testing, something is wrong
browser_output_timeout: 120 * 1000,
browser_open_timeout: 60 * 4 * 1000,
// we want to be notified something is wrong asap, so no retry
browser_retries: 1,
server: './test/support/server.js',
builder: 'zuul-builder-webpack',
webpack: require('./support/webpack.config.js')
};
if (process.env.CI === 'true') {
zuulConfig.local = false;
zuulConfig.tunnel = {
type: 'ngrok',
bind_tls: true
};
}
zuulConfig.browsers = [
{
name: 'firefox',
version: 'latest'
}, {
name: 'internet explorer',
version: '9..11'
}, {
name: 'safari',
version: '14'
}, {
name: 'iphone',
version: '14'
}, {
name: 'android',
version: '5.1..6.0'
}, {
name: 'ipad',
version: '14'
}, {
name: 'MicrosoftEdge',
version: 'latest'
}
];