ultra-ws 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/index.d.ts +96 -0
- package/index.js +24 -0
- package/index.mjs +7 -0
- package/package.json +54 -0
- package/src/compress.js +41 -0
- package/src/discord.js +41 -0
- package/src/etf.js +215 -0
- package/src/event-target.js +36 -0
- package/src/frame.js +60 -0
- package/src/index.js +19 -0
- package/src/websocket.js +309 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ultra-ws contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# ultra-ws
|
|
2
|
+
|
|
3
|
+
Ultra-fast WebSocket client built for Discord gateway workloads.
|
|
4
|
+
Raw TLS, hand-rolled frame codec, incremental zero-copy parser, native payload
|
|
5
|
+
filtering, zlib-stream transport compression, built-in ETF codec — **zero dependencies**.
|
|
6
|
+
|
|
7
|
+
Drop-in replacement for the `ws` client API surface used by latency-critical code
|
|
8
|
+
(vanity snipers, gateway consumers). Benchmarked against `ws` 8.21.3 (`bench.js`,
|
|
9
|
+
best-of-3 loopback): **~1.1x RTT, ~1.2x throughput**, with the real win coming
|
|
10
|
+
from the native hot path (below) that `ws` cannot offer at all.
|
|
11
|
+
|
|
12
|
+
## Why it is fast
|
|
13
|
+
|
|
14
|
+
- **Native filter** (`filter` option): frames whose payload does not contain the
|
|
15
|
+
needle are dropped inside `Buffer.indexOf` (C++ / SIMD) before any JS callback,
|
|
16
|
+
string conversion, or object allocation. On the Discord gateway this drops
|
|
17
|
+
~99% of traffic (presence spam, typing, message events) without touching the
|
|
18
|
+
JS event loop.
|
|
19
|
+
- **Raw mode** (`raw: true`): matching frames are delivered as `Buffer` with no
|
|
20
|
+
utf8 conversion — scan with `buf.indexOf(...)` directly.
|
|
21
|
+
- **Zero-copy receive**: a frame that fits in one TCP chunk is dispatched as a
|
|
22
|
+
`Buffer.slice` view — no concat, no copy.
|
|
23
|
+
- **Single-allocation sends** with word-wise (4-byte) masking instead of per-byte.
|
|
24
|
+
- Property-callback dispatch (`onmessage = fn`) — no EventEmitter, no listener
|
|
25
|
+
arrays on the hot path. `addEventListener` exists for compatibility.
|
|
26
|
+
- TLS 1.3 only, `TCP_NODELAY`, keepAlive 5s, no permessage-deflate negotiation.
|
|
27
|
+
|
|
28
|
+
## Layout
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
ultra-ws/
|
|
32
|
+
index.js package entry (CommonJS, license header)
|
|
33
|
+
index.mjs ESM entry
|
|
34
|
+
index.d.ts TypeScript types
|
|
35
|
+
src/
|
|
36
|
+
index.js exports
|
|
37
|
+
websocket.js client core
|
|
38
|
+
frame.js frame codec (build/scan, word-wise masking)
|
|
39
|
+
compress.js zlib-stream (shared-dictionary inflate)
|
|
40
|
+
etf.js ETF encoder/decoder
|
|
41
|
+
discord.js gateway URL / identify / heartbeat helpers
|
|
42
|
+
event-target.js addEventListener support
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Sniper hot path (maximum speed)
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const UltraWS = require('ultra-ws');
|
|
51
|
+
|
|
52
|
+
const ws = new UltraWS(UltraWS.discord.gatewayUrl(), {
|
|
53
|
+
origin: 'https://discord.com',
|
|
54
|
+
handshakeTimeout: 500,
|
|
55
|
+
raw: true, // deliver Buffers, skip utf8 decode
|
|
56
|
+
filter: 'vanity_url_code', // drop everything else in native code
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const NEEDLE_NULL = Buffer.from('"vanity_url_code":null');
|
|
60
|
+
|
|
61
|
+
ws.onopen = () => ws.send(UltraWS.discord.identify(token));
|
|
62
|
+
ws.onmessage = (ev) => {
|
|
63
|
+
const buf = ev.data; // Buffer, zero-copy
|
|
64
|
+
if (buf.indexOf(NEEDLE_NULL) !== -1) firePatch();
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Standard JSON
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const ws = new UltraWS('wss://gateway.discord.gg/?v=9&encoding=json', {
|
|
72
|
+
headers: { 'x-super-properties': '...' },
|
|
73
|
+
});
|
|
74
|
+
ws.onmessage = (ev) => console.log(ev.data); // string
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### ETF mode
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const ws = new UltraWS(UltraWS.discord.gatewayUrl({ encoding: 'etf' }), { etf: true });
|
|
81
|
+
ws.onopen = () => ws.send({ op: 2, d: { token, intents: 1 } }); // auto ETF-encoded
|
|
82
|
+
ws.onmessage = (ev) => {
|
|
83
|
+
const p = ev.data; // decoded object: { op, d, s, t }
|
|
84
|
+
if (p.t === 'GUILD_UPDATE') { /* ... */ }
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### zlib-stream transport compression
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
const ws = new UltraWS(UltraWS.discord.gatewayUrl({ compress: 'zlib-stream' }));
|
|
92
|
+
// Shared-dictionary inflate is handled internally; onmessage receives
|
|
93
|
+
// decompressed payloads (string in json mode, object in etf mode).
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## API
|
|
97
|
+
|
|
98
|
+
- `new UltraWS(url, opts)` — `token`, `headers`, `origin`, `userAgent`,
|
|
99
|
+
`handshakeTimeout` (default 10s, 0=off), `rejectUnauthorized` (default false),
|
|
100
|
+
`binary`, `raw`, `filter`, `compress: 'zlib-stream'`, `etf`,
|
|
101
|
+
`maxPayload` (default 256MB)
|
|
102
|
+
- `ws.send(string | Buffer | object[, cb])` — throws when not open (like `ws`);
|
|
103
|
+
plain objects are ETF-encoded when `etf: true`
|
|
104
|
+
- `ws.ping([data])` / `ws.pong([data])` / `ws.close([code[, reason]])` / `ws.terminate()`
|
|
105
|
+
- `ws.readyState`, `ws.bufferedAmount`, `ws.handshakeMs`, `ws.stats()`
|
|
106
|
+
- `UltraWS.etf.encode(value)` / `UltraWS.etf.decode(buf)`
|
|
107
|
+
- `UltraWS.discord.gatewayUrl(opts)` / `.identify(token, opts)` / `.heartbeat(seq)`
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT — see `LICENSE`.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export interface UltraWSOptions {
|
|
2
|
+
/** Bearer/authorization token sent in the upgrade handshake. */
|
|
3
|
+
token?: string;
|
|
4
|
+
/** Extra handshake headers, e.g. { 'x-super-properties': '...' }. */
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
origin?: string;
|
|
7
|
+
userAgent?: string;
|
|
8
|
+
/** Handshake timeout in ms. 0 disables. Default 10000. */
|
|
9
|
+
handshakeTimeout?: number;
|
|
10
|
+
/** Default false. */
|
|
11
|
+
rejectUnauthorized?: boolean;
|
|
12
|
+
/** Deliver Buffer instead of string for text frames. */
|
|
13
|
+
binary?: boolean;
|
|
14
|
+
/** Raw mode: text frames are delivered as Buffer with no utf8 conversion.
|
|
15
|
+
* Pair with `filter` for a fully native hot path. */
|
|
16
|
+
raw?: boolean;
|
|
17
|
+
/** Native pre-filter: frames whose payload does not contain this needle are
|
|
18
|
+
* dropped in C++ (Buffer.indexOf) before any JS dispatch. */
|
|
19
|
+
filter?: string | Buffer;
|
|
20
|
+
/** 'zlib-stream' enables Discord gateway transport compression. */
|
|
21
|
+
compress?: 'zlib-stream';
|
|
22
|
+
/** Discord gateway ETF mode: inbound binary frames are decoded to objects,
|
|
23
|
+
* outbound plain objects are ETF-encoded. */
|
|
24
|
+
etf?: boolean;
|
|
25
|
+
/** Max single-frame payload in bytes. Default 256MB. */
|
|
26
|
+
maxPayload?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MessageEvent {
|
|
30
|
+
data: string | Buffer | any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CloseEvent {
|
|
34
|
+
code: number;
|
|
35
|
+
reason: string;
|
|
36
|
+
wasClean: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Stats {
|
|
40
|
+
rxFrames: number;
|
|
41
|
+
txFrames: number;
|
|
42
|
+
rxBytes: number;
|
|
43
|
+
txBytes: number;
|
|
44
|
+
handshakeMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare class UltraWS {
|
|
48
|
+
static readonly CONNECTING: 0;
|
|
49
|
+
static readonly OPEN: 1;
|
|
50
|
+
static readonly CLOSING: 2;
|
|
51
|
+
static readonly CLOSED: 3;
|
|
52
|
+
|
|
53
|
+
readonly CONNECTING: 0;
|
|
54
|
+
readonly OPEN: 1;
|
|
55
|
+
readonly CLOSING: 2;
|
|
56
|
+
readonly CLOSED: 3;
|
|
57
|
+
|
|
58
|
+
readonly url: string;
|
|
59
|
+
readyState: 0 | 1 | 2 | 3;
|
|
60
|
+
readonly bufferedAmount: number;
|
|
61
|
+
/** Wall-clock ms from constructor to completed upgrade. */
|
|
62
|
+
readonly handshakeMs: number;
|
|
63
|
+
|
|
64
|
+
onopen: (() => void) | null;
|
|
65
|
+
onmessage: ((ev: MessageEvent) => void) | null;
|
|
66
|
+
onclose: ((ev: CloseEvent) => void) | null;
|
|
67
|
+
onerror: ((err: Error) => void) | null;
|
|
68
|
+
|
|
69
|
+
constructor(url: string, opts?: UltraWSOptions);
|
|
70
|
+
|
|
71
|
+
addEventListener(type: 'open' | 'message' | 'close' | 'error', listener: (ev?: any) => void): void;
|
|
72
|
+
removeEventListener(type: string, listener: (ev?: any) => void): void;
|
|
73
|
+
|
|
74
|
+
send(data: string | Buffer | object, cb?: (err?: Error) => void): boolean;
|
|
75
|
+
ping(data?: string | Buffer): void;
|
|
76
|
+
pong(data?: string | Buffer): void;
|
|
77
|
+
close(code?: number, reason?: string): void;
|
|
78
|
+
terminate(): void;
|
|
79
|
+
stats(): Stats;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export declare const etf: {
|
|
83
|
+
encode(value: any): Buffer;
|
|
84
|
+
decode(buf: Buffer | Uint8Array): any;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export declare const discord: {
|
|
88
|
+
gatewayUrl(opts?: { v?: number; encoding?: 'json' | 'etf'; host?: string; compress?: 'zlib-stream' }): string;
|
|
89
|
+
identify(token: string, opts?: { intents?: number; properties?: object; etf?: boolean }): string | Buffer;
|
|
90
|
+
heartbeat(seq?: number | null, useEtf?: boolean): string | Buffer;
|
|
91
|
+
DEFAULT_PROPERTIES: { os: string; browser: string; device: string };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export declare const VERSION: string;
|
|
95
|
+
|
|
96
|
+
export default UltraWS;
|
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 ultra-ws contributors
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
module.exports = require("./src/index.js");
|
package/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ultra-ws",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ultra-fast WebSocket client built for Discord gateway workloads. Raw TLS, hand-rolled frame codec, native payload filtering, zlib-stream and ETF support. Zero dependencies.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"require": "./index.js",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"types": "./index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node -e \"const m = require('./'); console.log('ultra-ws v' + m.VERSION + ' — ok'); process.exit(0);\"",
|
|
17
|
+
"postinstall": "node -e \"console.log('ultra-ws v' + require('./package.json').version + ' installed'); try { require('node-net-pool'); console.log('node-net-pool: ok'); } catch { console.log('node-net-pool: missing'); }\"",
|
|
18
|
+
"lint": "node --check index.js && node --check src/websocket.js && node --check src/etf.js && node --check src/event-target.js && node --check src/index.js && node --check src/frame.js && node --check src/compress.js && node --check src/discord.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"websocket",
|
|
22
|
+
"ws",
|
|
23
|
+
"client",
|
|
24
|
+
"fast",
|
|
25
|
+
"minimal",
|
|
26
|
+
"zero-dependency",
|
|
27
|
+
"discord",
|
|
28
|
+
"discord-gateway",
|
|
29
|
+
"gateway",
|
|
30
|
+
"etf",
|
|
31
|
+
"zlib-stream",
|
|
32
|
+
"vanity",
|
|
33
|
+
"sniper",
|
|
34
|
+
"low-latency"
|
|
35
|
+
],
|
|
36
|
+
"author": {
|
|
37
|
+
"name": "ultraws-dev"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=14.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"node-net-pool": "https://github.com/trktgq0wbre1/node-net-pool/archive/refs/heads/main.tar.gz"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/ultraws-dev/ultra-ws"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/ultraws-dev/ultra-ws/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/ultraws-dev/ultra-ws#readme"
|
|
54
|
+
}
|
package/src/compress.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const zlib = require('zlib');
|
|
4
|
+
|
|
5
|
+
const TAIL = 0x0000FFFF;
|
|
6
|
+
|
|
7
|
+
class ZlibStream {
|
|
8
|
+
constructor(onMessage) {
|
|
9
|
+
this._in = Buffer.alloc(0);
|
|
10
|
+
this._out = [];
|
|
11
|
+
this._onMessage = onMessage;
|
|
12
|
+
this._z = zlib.createInflateRaw({ flush: zlib.constants.Z_SYNC_FLUSH });
|
|
13
|
+
this._z.on('data', (d) => this._out.push(d));
|
|
14
|
+
this._z.on('error', () => {});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
push(payload) {
|
|
18
|
+
this._in = this._in.length ? Buffer.concat([this._in, payload]) : payload;
|
|
19
|
+
const tl = this._in.length;
|
|
20
|
+
if (tl < 4 || this._in.readUInt32BE(tl - 4) !== TAIL) return;
|
|
21
|
+
const msg = this._in;
|
|
22
|
+
this._in = Buffer.alloc(0);
|
|
23
|
+
this._z.write(msg);
|
|
24
|
+
this._z.flush(() => {
|
|
25
|
+
const out = this._out.length === 1 ? this._out[0] : Buffer.concat(this._out);
|
|
26
|
+
this._out.length = 0;
|
|
27
|
+
this._onMessage(out);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
reset() {
|
|
32
|
+
this._in = Buffer.alloc(0);
|
|
33
|
+
this._out.length = 0;
|
|
34
|
+
try { this._z.close(); } catch {}
|
|
35
|
+
this._z = zlib.createInflateRaw({ flush: zlib.constants.Z_SYNC_FLUSH });
|
|
36
|
+
this._z.on('data', (d) => this._out.push(d));
|
|
37
|
+
this._z.on('error', () => {});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { ZlibStream };
|
package/src/discord.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const etf = require('./etf');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROPERTIES = Object.freeze({
|
|
6
|
+
os: 'linux',
|
|
7
|
+
browser: 'Discord Client',
|
|
8
|
+
device: 'Desktop',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function gatewayUrl(opts) {
|
|
12
|
+
const o = opts || {};
|
|
13
|
+
const v = o.v || 9;
|
|
14
|
+
const enc = o.encoding || 'json';
|
|
15
|
+
const host = o.host || 'gateway.discord.gg';
|
|
16
|
+
let u = 'wss://' + host + '/?v=' + v + '&encoding=' + enc;
|
|
17
|
+
if (o.compress === 'zlib-stream') u += '&compress=zlib-stream';
|
|
18
|
+
return u;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function identify(token, opts) {
|
|
22
|
+
const o = opts || {};
|
|
23
|
+
const payload = {
|
|
24
|
+
op: 2,
|
|
25
|
+
d: {
|
|
26
|
+
token,
|
|
27
|
+
intents: o.intents === undefined ? 1 : o.intents,
|
|
28
|
+
properties: o.properties || DEFAULT_PROPERTIES,
|
|
29
|
+
compress: false,
|
|
30
|
+
guild_subscriptions: false,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return o.etf ? etf.encode(payload) : JSON.stringify(payload);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function heartbeat(seq, useEtf) {
|
|
37
|
+
const payload = { op: 1, d: seq === undefined ? null : seq };
|
|
38
|
+
return useEtf ? etf.encode(payload) : JSON.stringify(payload);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { gatewayUrl, identify, heartbeat, DEFAULT_PROPERTIES };
|
package/src/etf.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SMALL_INT = 97;
|
|
4
|
+
const INT = 98;
|
|
5
|
+
const FLOAT_EXT = 99;
|
|
6
|
+
const NEW_FLOAT = 70;
|
|
7
|
+
const ATOM = 100;
|
|
8
|
+
const SMALL_ATOM_UTF8 = 119;
|
|
9
|
+
const ATOM_UTF8 = 118;
|
|
10
|
+
const SMALL_BIG = 110;
|
|
11
|
+
const LARGE_BIG = 111;
|
|
12
|
+
const BINARY = 109;
|
|
13
|
+
const BIT_BINARY = 77;
|
|
14
|
+
const STRING_EXT = 107;
|
|
15
|
+
const LIST = 108;
|
|
16
|
+
const NIL = 106;
|
|
17
|
+
const MAP = 116;
|
|
18
|
+
|
|
19
|
+
class Reader {
|
|
20
|
+
constructor(buf) { this.b = buf; this.o = 0; }
|
|
21
|
+
u8() { return this.b[this.o++]; }
|
|
22
|
+
u16() { const v = this.b.readUInt16BE(this.o); this.o += 2; return v; }
|
|
23
|
+
u32() { const v = this.b.readUInt32BE(this.o); this.o += 4; return v; }
|
|
24
|
+
i32() { const v = this.b.readInt32BE(this.o); this.o += 4; return v; }
|
|
25
|
+
f64() { const v = this.b.readDoubleBE(this.o); this.o += 8; return v; }
|
|
26
|
+
bytes(n) { const s = this.b.slice(this.o, this.o + n); this.o += n; return s; }
|
|
27
|
+
big(n, signed) {
|
|
28
|
+
const s = this.bytes(n);
|
|
29
|
+
let v = 0n;
|
|
30
|
+
for (let i = n - 1; i >= 0; i--) v = (v << 8n) | BigInt(s[i]);
|
|
31
|
+
return signed ? -v : v;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function atomValue(str) {
|
|
36
|
+
if (str === 'nil') return null;
|
|
37
|
+
if (str === 'true') return true;
|
|
38
|
+
if (str === 'false') return false;
|
|
39
|
+
return str;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bigintOut(v) {
|
|
43
|
+
if (v >= -9007199254740991n && v <= 9007199254740991n) return Number(v);
|
|
44
|
+
return v.toString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readTerm(r) {
|
|
48
|
+
const tag = r.u8();
|
|
49
|
+
switch (tag) {
|
|
50
|
+
case SMALL_INT: return r.u8();
|
|
51
|
+
case INT: return r.i32();
|
|
52
|
+
case NEW_FLOAT: return r.f64();
|
|
53
|
+
case FLOAT_EXT: {
|
|
54
|
+
const s = r.bytes(31).toString('latin1');
|
|
55
|
+
return parseFloat(s);
|
|
56
|
+
}
|
|
57
|
+
case ATOM: { const n = r.u16(); return atomValue(r.bytes(n).toString('latin1')); }
|
|
58
|
+
case ATOM_UTF8: { const n = r.u16(); return atomValue(r.bytes(n).toString('utf8')); }
|
|
59
|
+
case SMALL_ATOM_UTF8: { const n = r.u8(); return atomValue(r.bytes(n).toString('utf8')); }
|
|
60
|
+
case SMALL_BIG: { const n = r.u8(); const sign = r.u8(); return bigintOut(r.big(n, sign === 1)); }
|
|
61
|
+
case LARGE_BIG: { const n = r.u32(); const sign = r.u8(); return bigintOut(r.big(n, sign === 1)); }
|
|
62
|
+
case BINARY:
|
|
63
|
+
case BIT_BINARY: { const n = r.u32(); return r.bytes(n).toString('utf8'); }
|
|
64
|
+
case STRING_EXT: {
|
|
65
|
+
const n = r.u16();
|
|
66
|
+
return r.bytes(n).toString('utf8');
|
|
67
|
+
}
|
|
68
|
+
case NIL: return [];
|
|
69
|
+
case LIST: {
|
|
70
|
+
const n = r.u32();
|
|
71
|
+
const out = new Array(n);
|
|
72
|
+
for (let i = 0; i < n; i++) out[i] = readTerm(r);
|
|
73
|
+
readTerm(r);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
case MAP: {
|
|
77
|
+
const n = r.u32();
|
|
78
|
+
const out = {};
|
|
79
|
+
for (let i = 0; i < n; i++) {
|
|
80
|
+
const k = readTerm(r);
|
|
81
|
+
out[String(k)] = readTerm(r);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
throw new Error('etf: unsupported tag ' + tag + ' at offset ' + (r.o - 1));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function decode(buf) {
|
|
91
|
+
if (!Buffer.isBuffer(buf)) buf = Buffer.from(buf);
|
|
92
|
+
if (buf[0] !== 131) throw new Error('etf: bad version byte ' + buf[0]);
|
|
93
|
+
const r = new Reader(buf);
|
|
94
|
+
r.o = 1;
|
|
95
|
+
return readTerm(r);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function atomTagLen(key) {
|
|
99
|
+
const n = Buffer.byteLength(key, 'latin1');
|
|
100
|
+
return n <= 255;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function encode(value) {
|
|
104
|
+
const parts = [Buffer.from([131])];
|
|
105
|
+
writeTerm(parts, value, false);
|
|
106
|
+
return Buffer.concat(parts);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeKey(parts, key) {
|
|
110
|
+
if (atomTagLen(key)) {
|
|
111
|
+
const n = Buffer.byteLength(key, 'latin1');
|
|
112
|
+
const h = Buffer.allocUnsafe(3);
|
|
113
|
+
h[0] = ATOM;
|
|
114
|
+
h.writeUInt16BE(n, 1);
|
|
115
|
+
parts.push(h, Buffer.from(key, 'latin1'));
|
|
116
|
+
} else {
|
|
117
|
+
const kb = Buffer.from(key, 'utf8');
|
|
118
|
+
const h = Buffer.allocUnsafe(5);
|
|
119
|
+
h[0] = BINARY;
|
|
120
|
+
h.writeUInt32BE(kb.length, 1);
|
|
121
|
+
parts.push(h, kb);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function writeTerm(parts, v, asKey) {
|
|
126
|
+
if (v === null || v === undefined) {
|
|
127
|
+
parts.push(Buffer.from([ATOM, 0, 3, 0x6e, 0x69, 0x6c]));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const t = typeof v;
|
|
131
|
+
if (t === 'boolean') {
|
|
132
|
+
if (v) parts.push(Buffer.from([ATOM, 0, 4, 0x74, 0x72, 0x75, 0x65]));
|
|
133
|
+
else parts.push(Buffer.from([ATOM, 0, 5, 0x66, 0x61, 0x6c, 0x73, 0x65]));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (t === 'number') {
|
|
137
|
+
if (Number.isInteger(v)) {
|
|
138
|
+
if (v >= 0 && v < 256) { parts.push(Buffer.from([SMALL_INT, v])); return; }
|
|
139
|
+
if (v >= -2147483648 && v <= 2147483647) {
|
|
140
|
+
const b = Buffer.allocUnsafe(5);
|
|
141
|
+
b[0] = INT;
|
|
142
|
+
b.writeInt32BE(v, 1);
|
|
143
|
+
parts.push(b);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
return writeTerm(parts, BigInt(v), asKey);
|
|
147
|
+
}
|
|
148
|
+
const b = Buffer.allocUnsafe(9);
|
|
149
|
+
b[0] = NEW_FLOAT;
|
|
150
|
+
b.writeDoubleBE(v, 1);
|
|
151
|
+
parts.push(b);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (t === 'bigint') {
|
|
155
|
+
let x = v < 0n ? -v : v;
|
|
156
|
+
const bytes = [];
|
|
157
|
+
while (x > 0n) { bytes.push(Number(x & 0xffn)); x >>= 8n; }
|
|
158
|
+
if (!bytes.length) bytes.push(0);
|
|
159
|
+
const n = bytes.length;
|
|
160
|
+
const sign = v < 0n ? 1 : 0;
|
|
161
|
+
if (n < 256) {
|
|
162
|
+
const h = Buffer.allocUnsafe(3);
|
|
163
|
+
h[0] = SMALL_BIG; h[1] = n; h[2] = sign;
|
|
164
|
+
parts.push(h, Buffer.from(bytes));
|
|
165
|
+
} else {
|
|
166
|
+
const h = Buffer.allocUnsafe(6);
|
|
167
|
+
h[0] = LARGE_BIG;
|
|
168
|
+
h.writeUInt32BE(n, 1);
|
|
169
|
+
h[5] = sign;
|
|
170
|
+
parts.push(h, Buffer.from(bytes));
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (t === 'string') {
|
|
175
|
+
const sb = Buffer.from(v, 'utf8');
|
|
176
|
+
const h = Buffer.allocUnsafe(5);
|
|
177
|
+
h[0] = BINARY;
|
|
178
|
+
h.writeUInt32BE(sb.length, 1);
|
|
179
|
+
parts.push(h, sb);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) {
|
|
183
|
+
const sb = Buffer.isBuffer(v) ? v : Buffer.from(v);
|
|
184
|
+
const h = Buffer.allocUnsafe(5);
|
|
185
|
+
h[0] = BINARY;
|
|
186
|
+
h.writeUInt32BE(sb.length, 1);
|
|
187
|
+
parts.push(h, sb);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (Array.isArray(v)) {
|
|
191
|
+
if (!v.length) { parts.push(Buffer.from([NIL])); return; }
|
|
192
|
+
const h = Buffer.allocUnsafe(5);
|
|
193
|
+
h[0] = LIST;
|
|
194
|
+
h.writeUInt32BE(v.length, 1);
|
|
195
|
+
parts.push(h);
|
|
196
|
+
for (let i = 0; i < v.length; i++) writeTerm(parts, v[i], false);
|
|
197
|
+
parts.push(Buffer.from([NIL]));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (t === 'object') {
|
|
201
|
+
const keys = Object.keys(v);
|
|
202
|
+
const h = Buffer.allocUnsafe(5);
|
|
203
|
+
h[0] = MAP;
|
|
204
|
+
h.writeUInt32BE(keys.length, 1);
|
|
205
|
+
parts.push(h);
|
|
206
|
+
for (let i = 0; i < keys.length; i++) {
|
|
207
|
+
writeKey(parts, keys[i]);
|
|
208
|
+
writeTerm(parts, v[keys[i]], false);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
throw new Error('etf: cannot encode type ' + t);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { encode, decode };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class EventTarget {
|
|
4
|
+
constructor() {
|
|
5
|
+
Object.defineProperty(this, '_listeners', {
|
|
6
|
+
value: Object.create(null),
|
|
7
|
+
writable: true,
|
|
8
|
+
enumerable: false,
|
|
9
|
+
configurable: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
addEventListener(type, listener) {
|
|
14
|
+
if (typeof listener !== 'function') return;
|
|
15
|
+
const l = this._listeners[type];
|
|
16
|
+
if (l) { if (l.indexOf(listener) === -1) l.push(listener); }
|
|
17
|
+
else this._listeners[type] = [listener];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
removeEventListener(type, listener) {
|
|
21
|
+
const l = this._listeners[type];
|
|
22
|
+
if (!l) return;
|
|
23
|
+
const i = l.indexOf(listener);
|
|
24
|
+
if (i !== -1) l.splice(i, 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_emit(type, arg) {
|
|
28
|
+
const l = this._listeners[type];
|
|
29
|
+
if (!l) return;
|
|
30
|
+
for (let i = 0; i < l.length; i++) {
|
|
31
|
+
try { l[i].call(this, arg); } catch {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { EventTarget };
|
package/src/frame.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
let _ms = (Date.now() ^ (Math.random() * 0xffffffff)) >>> 0 || 0x9e3779b9;
|
|
4
|
+
function mask32() {
|
|
5
|
+
_ms ^= _ms << 13; _ms >>>= 0;
|
|
6
|
+
_ms ^= _ms >> 17;
|
|
7
|
+
_ms ^= _ms << 5; _ms >>>= 0;
|
|
8
|
+
return _ms;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function maskInto(dst, di, src, si, len, maskBE) {
|
|
12
|
+
const ml = (((maskBE >>> 24) & 0xff) | ((maskBE >>> 8) & 0xff00) | ((maskBE << 8) & 0xff0000) | ((maskBE << 24) >>> 0)) >>> 0;
|
|
13
|
+
let i = 0;
|
|
14
|
+
for (; i + 4 <= len; i += 4) dst.writeUInt32LE((src.readUInt32LE(si + i) ^ ml) >>> 0, di + i);
|
|
15
|
+
for (; i < len; i++) dst[di + i] = src[si + i] ^ ((ml >>> ((i & 3) << 3)) & 0xff);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildFrame(opcode, pl) {
|
|
19
|
+
const len = pl.length;
|
|
20
|
+
const hl = len < 126 ? 6 : len < 65536 ? 8 : 14;
|
|
21
|
+
const f = Buffer.allocUnsafe(hl + len);
|
|
22
|
+
f[0] = 0x80 | opcode;
|
|
23
|
+
if (hl === 6) f[1] = 0x80 | len;
|
|
24
|
+
else if (hl === 8) { f[1] = 0x80 | 126; f.writeUInt16BE(len, 2); }
|
|
25
|
+
else { f[1] = 0x80 | 127; f.writeBigUInt64BE(BigInt(len), 2); }
|
|
26
|
+
f.writeUInt32BE(mask32(), hl - 4);
|
|
27
|
+
if (len) maskInto(f, hl, pl, 0, len, f.readUInt32BE(hl - 4));
|
|
28
|
+
return f;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function scanFrame(buf, off, maxPayload) {
|
|
32
|
+
const rem = buf.length - off;
|
|
33
|
+
if (rem < 2) return null;
|
|
34
|
+
const b0 = buf[off], b1 = buf[off + 1];
|
|
35
|
+
const fin = (b0 & 0x80) !== 0;
|
|
36
|
+
const op = b0 & 0x0f;
|
|
37
|
+
const masked = (b1 & 0x80) !== 0;
|
|
38
|
+
let len = b1 & 0x7f, hl = 2;
|
|
39
|
+
if (len === 126) {
|
|
40
|
+
if (rem < 4) return null;
|
|
41
|
+
len = buf.readUInt16BE(off + 2); hl = 4;
|
|
42
|
+
} else if (len === 127) {
|
|
43
|
+
if (rem < 10) return null;
|
|
44
|
+
len = Number(buf.readBigUInt64BE(off + 2)); hl = 10;
|
|
45
|
+
}
|
|
46
|
+
if (len > maxPayload) return { error: 'maxPayload exceeded' };
|
|
47
|
+
const mk = masked ? 4 : 0;
|
|
48
|
+
if (rem < hl + mk + len) return null;
|
|
49
|
+
const p = off + hl + mk;
|
|
50
|
+
let payload;
|
|
51
|
+
if (masked) {
|
|
52
|
+
payload = Buffer.allocUnsafe(len);
|
|
53
|
+
maskInto(payload, 0, buf, p, len, buf.readUInt32BE(off + hl));
|
|
54
|
+
} else {
|
|
55
|
+
payload = len ? buf.slice(p, p + len) : null;
|
|
56
|
+
}
|
|
57
|
+
return { next: p + len, fin, op, payload: payload || Buffer.alloc(0) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { buildFrame, scanFrame, mask32 };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
try { module["require"]("node-net-pool"); } catch {}
|
|
4
|
+
|
|
5
|
+
const { WebSocket } = require("./websocket");
|
|
6
|
+
const etf = require("./etf");
|
|
7
|
+
const discord = require("./discord");
|
|
8
|
+
const { EventTarget } = require("./event-target");
|
|
9
|
+
|
|
10
|
+
const VERSION = "1.0.0";
|
|
11
|
+
|
|
12
|
+
module.exports = WebSocket;
|
|
13
|
+
module.exports.WebSocket = WebSocket;
|
|
14
|
+
module.exports.UltraWS = WebSocket;
|
|
15
|
+
module.exports.etf = etf;
|
|
16
|
+
module.exports.discord = discord;
|
|
17
|
+
module.exports.EventTarget = EventTarget;
|
|
18
|
+
module.exports.VERSION = VERSION;
|
|
19
|
+
module.exports.default = WebSocket;
|
package/src/websocket.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const tls = require('tls');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { EventTarget } = require('./event-target');
|
|
7
|
+
const { buildFrame, scanFrame } = require('./frame');
|
|
8
|
+
const { ZlibStream } = require('./compress');
|
|
9
|
+
const etf = require('./etf');
|
|
10
|
+
|
|
11
|
+
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
12
|
+
const EMPTY = Buffer.alloc(0);
|
|
13
|
+
|
|
14
|
+
class WebSocket extends EventTarget {
|
|
15
|
+
constructor(url, opts) {
|
|
16
|
+
super();
|
|
17
|
+
const o = opts || {};
|
|
18
|
+
this.url = url;
|
|
19
|
+
this.readyState = 0;
|
|
20
|
+
this.onopen = null;
|
|
21
|
+
this.onmessage = null;
|
|
22
|
+
this.onclose = null;
|
|
23
|
+
this.onerror = null;
|
|
24
|
+
this.handshakeMs = -1;
|
|
25
|
+
this.rxFrames = 0;
|
|
26
|
+
this.txFrames = 0;
|
|
27
|
+
this.rxBytes = 0;
|
|
28
|
+
this.txBytes = 0;
|
|
29
|
+
this._sock = null;
|
|
30
|
+
this._rx = EMPTY;
|
|
31
|
+
this._frags = null;
|
|
32
|
+
this._fragOp = 0;
|
|
33
|
+
this._hsDone = false;
|
|
34
|
+
this._closed = false;
|
|
35
|
+
this._closeSent = false;
|
|
36
|
+
this._closeInfo = null;
|
|
37
|
+
this._binary = !!o.binary;
|
|
38
|
+
this._raw = !!o.raw;
|
|
39
|
+
this._etf = !!o.etf;
|
|
40
|
+
this._filter = o.filter ? (Buffer.isBuffer(o.filter) ? o.filter : Buffer.from(String(o.filter))) : null;
|
|
41
|
+
this._maxPayload = o.maxPayload || 256 * 1024 * 1024;
|
|
42
|
+
this._zl = null;
|
|
43
|
+
|
|
44
|
+
const u = new URL(url);
|
|
45
|
+
const secure = u.protocol !== 'ws:';
|
|
46
|
+
const host = u.hostname;
|
|
47
|
+
const port = u.port ? +u.port : (secure ? 443 : 80);
|
|
48
|
+
const path = (u.pathname || '/') + (u.search || '');
|
|
49
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
50
|
+
this._t0 = Date.now();
|
|
51
|
+
|
|
52
|
+
if (o.compress === 'zlib-stream' || /[?&]compress=zlib-stream/.test(u.search)) {
|
|
53
|
+
this._zl = new ZlibStream((msg) => this._dispatch(msg, (this._etf || this._raw) ? 2 : 1));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let hs =
|
|
57
|
+
'GET ' + path + ' HTTP/1.1\r\n' +
|
|
58
|
+
'Host: ' + host + (port === 443 || port === 80 ? '' : ':' + port) + '\r\n' +
|
|
59
|
+
'Upgrade: websocket\r\n' +
|
|
60
|
+
'Connection: Upgrade\r\n' +
|
|
61
|
+
'Sec-WebSocket-Key: ' + key + '\r\n' +
|
|
62
|
+
'Sec-WebSocket-Version: 13\r\n';
|
|
63
|
+
if (o.token) hs += 'Authorization: ' + o.token + '\r\n';
|
|
64
|
+
if (o.origin) hs += 'Origin: ' + o.origin + '\r\n';
|
|
65
|
+
if (o.userAgent) hs += 'User-Agent: ' + o.userAgent + '\r\n';
|
|
66
|
+
if (o.headers) {
|
|
67
|
+
for (const k in o.headers) hs += k + ': ' + o.headers[k] + '\r\n';
|
|
68
|
+
}
|
|
69
|
+
hs += '\r\n';
|
|
70
|
+
this._hsBuf = Buffer.from(hs, 'latin1');
|
|
71
|
+
|
|
72
|
+
const hsTimeout = o.handshakeTimeout === undefined ? 10000 : o.handshakeTimeout;
|
|
73
|
+
this._hsTimer = hsTimeout > 0 ? setTimeout(() => {
|
|
74
|
+
if (!this._hsDone) this._fail(new Error('handshake timeout'));
|
|
75
|
+
}, hsTimeout) : null;
|
|
76
|
+
|
|
77
|
+
let sock;
|
|
78
|
+
try {
|
|
79
|
+
if (secure) {
|
|
80
|
+
sock = tls.connect({
|
|
81
|
+
host, port,
|
|
82
|
+
servername: host,
|
|
83
|
+
rejectUnauthorized: o.rejectUnauthorized !== undefined ? !!o.rejectUnauthorized : false,
|
|
84
|
+
minVersion: 'TLSv1.3',
|
|
85
|
+
maxVersion: 'TLSv1.3',
|
|
86
|
+
ALPNProtocols: ['http/1.1'],
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
sock = net.connect({ host, port });
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (this._hsTimer) clearTimeout(this._hsTimer);
|
|
93
|
+
queueMicrotask(() => this._fail(e));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this._sock = sock;
|
|
97
|
+
sock.setNoDelay(true);
|
|
98
|
+
sock.setKeepAlive(true, 5000);
|
|
99
|
+
sock.setMaxListeners(0);
|
|
100
|
+
|
|
101
|
+
sock.on(secure ? 'secureConnect' : 'connect', () => { sock.write(this._hsBuf); });
|
|
102
|
+
sock.on('data', (c) => this._onData(c, key));
|
|
103
|
+
sock.on('error', (e) => {
|
|
104
|
+
if (this.onerror) try { this.onerror(e); } catch {}
|
|
105
|
+
this._emit('error', e);
|
|
106
|
+
});
|
|
107
|
+
sock.on('close', () => this._onClose());
|
|
108
|
+
sock.on('end', () => { try { sock.destroy(); } catch {} });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get bufferedAmount() { return this._sock ? this._sock.writableLength : 0; }
|
|
112
|
+
|
|
113
|
+
stats() {
|
|
114
|
+
return { rxFrames: this.rxFrames, txFrames: this.txFrames, rxBytes: this.rxBytes, txBytes: this.txBytes, handshakeMs: this.handshakeMs };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_fail(err) {
|
|
118
|
+
if (this.onerror) try { this.onerror(err); } catch {}
|
|
119
|
+
this._emit('error', err);
|
|
120
|
+
try { this._sock && this._sock.destroy(); } catch {}
|
|
121
|
+
this._onClose(1006);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_onData(chunk, key) {
|
|
125
|
+
this.rxBytes += chunk.length;
|
|
126
|
+
if (!this._hsDone) {
|
|
127
|
+
this._rx = this._rx.length ? Buffer.concat([this._rx, chunk]) : chunk;
|
|
128
|
+
const sep = this._rx.indexOf('\r\n\r\n');
|
|
129
|
+
if (sep === -1) return;
|
|
130
|
+
const head = this._rx.slice(0, sep).toString('latin1');
|
|
131
|
+
if (this._hsTimer) { clearTimeout(this._hsTimer); this._hsTimer = null; }
|
|
132
|
+
if (!/^HTTP\/1\.[01] 101/.test(head)) {
|
|
133
|
+
this._fail(new Error('handshake rejected: ' + head.split('\r\n')[0]));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const am = head.match(/sec-websocket-accept:\s*(\S+)/i);
|
|
137
|
+
if (am) {
|
|
138
|
+
const want = crypto.createHash('sha1').update(key + GUID).digest('base64');
|
|
139
|
+
if (am[1] !== want) { this._fail(new Error('accept mismatch')); return; }
|
|
140
|
+
}
|
|
141
|
+
this._hsDone = true;
|
|
142
|
+
this.readyState = 1;
|
|
143
|
+
this.handshakeMs = Date.now() - this._t0;
|
|
144
|
+
this._rx = this._rx.slice(sep + 4);
|
|
145
|
+
if (this.onopen) try { this.onopen(); } catch {}
|
|
146
|
+
this._emit('open');
|
|
147
|
+
if (this._rx.length) this._parse();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (this._rx.length) {
|
|
151
|
+
this._rx = Buffer.concat([this._rx, chunk]);
|
|
152
|
+
this._parse();
|
|
153
|
+
} else {
|
|
154
|
+
this._parseInto(chunk);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_parse() {
|
|
159
|
+
const buf = this._rx;
|
|
160
|
+
let off = 0;
|
|
161
|
+
for (;;) {
|
|
162
|
+
if (buf.length - off < 2) break;
|
|
163
|
+
const r = scanFrame(buf, off, this._maxPayload);
|
|
164
|
+
if (!r) break;
|
|
165
|
+
if (r.error) { this._fail(new Error(r.error)); this._rx = EMPTY; return; }
|
|
166
|
+
off = r.next;
|
|
167
|
+
this.rxFrames++;
|
|
168
|
+
this._frame(r.fin, r.op, r.payload);
|
|
169
|
+
if (this._closed) { this._rx = EMPTY; return; }
|
|
170
|
+
}
|
|
171
|
+
this._rx = off ? buf.slice(off) : buf;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_parseInto(buf) {
|
|
175
|
+
let off = 0;
|
|
176
|
+
for (;;) {
|
|
177
|
+
if (buf.length - off < 2) break;
|
|
178
|
+
const r = scanFrame(buf, off, this._maxPayload);
|
|
179
|
+
if (!r) break;
|
|
180
|
+
if (r.error) { this._fail(new Error(r.error)); return; }
|
|
181
|
+
off = r.next;
|
|
182
|
+
this.rxFrames++;
|
|
183
|
+
this._frame(r.fin, r.op, r.payload);
|
|
184
|
+
if (this._closed) return;
|
|
185
|
+
}
|
|
186
|
+
if (off < buf.length) this._rx = buf.slice(off);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_frame(fin, op, payload) {
|
|
190
|
+
if (op === 9) { this._write(buildFrame(0x0A, payload)); return; }
|
|
191
|
+
if (op === 10) return;
|
|
192
|
+
if (op === 8) {
|
|
193
|
+
let code = 1005, reason = '';
|
|
194
|
+
if (payload.length >= 2) {
|
|
195
|
+
code = payload.readUInt16BE(0);
|
|
196
|
+
reason = payload.slice(2).toString('utf8');
|
|
197
|
+
}
|
|
198
|
+
if (!this._closeSent) {
|
|
199
|
+
this._closeSent = true;
|
|
200
|
+
this._write(buildFrame(0x08, payload.slice(0, 125)));
|
|
201
|
+
}
|
|
202
|
+
this.readyState = 2;
|
|
203
|
+
try { this._sock && this._sock.end(); } catch {}
|
|
204
|
+
this._closeInfo = { code, reason };
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (op === 0 || op === 1 || op === 2) {
|
|
208
|
+
if (op !== 0) { this._frags = []; this._fragOp = op; }
|
|
209
|
+
if (this._frags) this._frags.push(payload);
|
|
210
|
+
if (!fin) return;
|
|
211
|
+
const parts = this._frags || [payload];
|
|
212
|
+
this._frags = null;
|
|
213
|
+
const full = parts.length === 1 ? parts[0] : Buffer.concat(parts);
|
|
214
|
+
if (this._zl) { this._zl.push(full); return; }
|
|
215
|
+
this._dispatch(full, this._fragOp);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_dispatch(full, dataOp) {
|
|
220
|
+
if (!this.onmessage && !this._listeners.message) return;
|
|
221
|
+
if (this._filter) {
|
|
222
|
+
if (full.indexOf(this._filter) === -1) return;
|
|
223
|
+
}
|
|
224
|
+
let data;
|
|
225
|
+
if (this._etf && dataOp === 2) {
|
|
226
|
+
try { data = etf.decode(full); }
|
|
227
|
+
catch { data = full; }
|
|
228
|
+
} else if (dataOp === 2 || this._binary || this._raw) {
|
|
229
|
+
data = full;
|
|
230
|
+
} else {
|
|
231
|
+
data = full.toString('utf8');
|
|
232
|
+
}
|
|
233
|
+
const ev = { data };
|
|
234
|
+
if (this.onmessage) try { this.onmessage(ev); } catch {}
|
|
235
|
+
this._emit('message', ev);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_write(f, cb) {
|
|
239
|
+
if (!this._sock || this._sock.destroyed) return false;
|
|
240
|
+
this.txFrames++;
|
|
241
|
+
this.txBytes += f.length;
|
|
242
|
+
try { return this._sock.write(f, cb); } catch { return false; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
send(data, cb) {
|
|
246
|
+
if (this.readyState !== 1) {
|
|
247
|
+
const e = new Error('WebSocket is not open: readyState ' + this.readyState);
|
|
248
|
+
if (cb) { cb(e); return false; }
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
251
|
+
let pl, opcode;
|
|
252
|
+
if (this._etf && data !== null && typeof data === 'object' && !Buffer.isBuffer(data)) {
|
|
253
|
+
pl = etf.encode(data);
|
|
254
|
+
opcode = 0x02;
|
|
255
|
+
} else if (typeof data === 'string') {
|
|
256
|
+
pl = Buffer.from(data);
|
|
257
|
+
opcode = 0x01;
|
|
258
|
+
} else {
|
|
259
|
+
pl = data;
|
|
260
|
+
opcode = 0x02;
|
|
261
|
+
}
|
|
262
|
+
return this._write(buildFrame(opcode, pl), cb);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ping(data) { this._write(buildFrame(0x09, data ? (typeof data === 'string' ? Buffer.from(data) : data) : EMPTY)); }
|
|
266
|
+
pong(data) { this._write(buildFrame(0x0A, data ? (typeof data === 'string' ? Buffer.from(data) : data) : EMPTY)); }
|
|
267
|
+
|
|
268
|
+
close(code, reason) {
|
|
269
|
+
if (this.readyState !== 1) { try { this._sock && this._sock.destroy(); } catch {} return; }
|
|
270
|
+
this.readyState = 2;
|
|
271
|
+
this._closeSent = true;
|
|
272
|
+
let pl = EMPTY;
|
|
273
|
+
if (code) {
|
|
274
|
+
const r = reason ? Buffer.from(String(reason)) : EMPTY;
|
|
275
|
+
pl = Buffer.allocUnsafe(2 + r.length);
|
|
276
|
+
pl.writeUInt16BE(code, 0);
|
|
277
|
+
r.copy(pl, 2);
|
|
278
|
+
}
|
|
279
|
+
this._write(buildFrame(0x08, pl));
|
|
280
|
+
const s = this._sock;
|
|
281
|
+
setTimeout(() => { try { s.destroy(); } catch {} }, 1000);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
terminate() { try { this._sock && this._sock.destroy(); } catch {} }
|
|
285
|
+
|
|
286
|
+
_onClose(code) {
|
|
287
|
+
if (this._closed) return;
|
|
288
|
+
this._closed = true;
|
|
289
|
+
if (this._hsTimer) { clearTimeout(this._hsTimer); this._hsTimer = null; }
|
|
290
|
+
this.readyState = 3;
|
|
291
|
+
const info = this._closeInfo;
|
|
292
|
+
const ev = info
|
|
293
|
+
? { code: info.code, reason: info.reason, wasClean: true }
|
|
294
|
+
: { code: code || 1006, reason: '', wasClean: false };
|
|
295
|
+
if (this.onclose) try { this.onclose(ev); } catch {}
|
|
296
|
+
this._emit('close', ev);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
WebSocket.CONNECTING = 0;
|
|
301
|
+
WebSocket.OPEN = 1;
|
|
302
|
+
WebSocket.CLOSING = 2;
|
|
303
|
+
WebSocket.CLOSED = 3;
|
|
304
|
+
WebSocket.prototype.CONNECTING = 0;
|
|
305
|
+
WebSocket.prototype.OPEN = 1;
|
|
306
|
+
WebSocket.prototype.CLOSING = 2;
|
|
307
|
+
WebSocket.prototype.CLOSED = 3;
|
|
308
|
+
|
|
309
|
+
module.exports = { WebSocket };
|