quico 0.3.0 → 0.7.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/README.md +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/ws_server.js
ADDED
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: a full HTTP stack in JavaScript
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://github.com/colocohen/quico
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Blob } from 'node:buffer';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
WSParser, encodeFrame, acceptKey,
|
|
16
|
+
isValidUserCloseCode, OPCODE, CLOSE,
|
|
17
|
+
installEventSurfaces, emitBoth, nodeMessagePayload
|
|
18
|
+
} from './ws.js';
|
|
19
|
+
import { isToken, STATUS_TEXT } from './http_common.js';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// WebSocket server.
|
|
24
|
+
//
|
|
25
|
+
// The connection object handed to the application is the SAME WebSocket the
|
|
26
|
+
// browser defines and the same one ws_client.js produces — same events, same
|
|
27
|
+
// readyState, same close semantics, same binaryType. A function written to
|
|
28
|
+
// handle a socket works unchanged on either side, which is the whole reason
|
|
29
|
+
// for choosing the browser interface over the `ws` package's shape.
|
|
30
|
+
//
|
|
31
|
+
// What has no browser equivalent is *accepting* a connection, because a
|
|
32
|
+
// browser never does. That part is modelled on Deno and Cloudflare Workers,
|
|
33
|
+
// which return a standard WebSocket from a server-side upgrade — and on the
|
|
34
|
+
// `ws` package for the handshake validation itself, which is worth copying
|
|
35
|
+
// precisely: it answers a bad handshake with a real HTTP response rather than
|
|
36
|
+
// dropping the socket, so a client learns WHY it was refused instead of
|
|
37
|
+
// seeing a connection reset.
|
|
38
|
+
// ============================================================
|
|
39
|
+
|
|
40
|
+
var CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3;
|
|
41
|
+
var CLOSE_TIMEOUT = 30000;
|
|
42
|
+
|
|
43
|
+
// A Sec-WebSocket-Key is 16 random bytes in base64 — always 24 characters
|
|
44
|
+
// ending in '=='. Checking the shape (as `ws` does) catches a client that is
|
|
45
|
+
// not really speaking WebSocket before any state is built for it.
|
|
46
|
+
var KEY_RE = /^[+/0-9A-Za-z]{22}==$/;
|
|
47
|
+
|
|
48
|
+
var CloseEventCtor = typeof CloseEvent !== 'undefined' ? CloseEvent : function (type, init) {
|
|
49
|
+
init = init || {};
|
|
50
|
+
var ev = new Event(type, init);
|
|
51
|
+
Object.defineProperties(ev, {
|
|
52
|
+
code: { value: init.code === undefined ? 0 : init.code, enumerable: true },
|
|
53
|
+
reason: { value: init.reason === undefined ? '' : init.reason, enumerable: true },
|
|
54
|
+
wasClean: { value: !!init.wasClean, enumerable: true }
|
|
55
|
+
});
|
|
56
|
+
return ev;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
// ============================================================
|
|
61
|
+
// ServerWebSocket — the WHATWG interface, server side
|
|
62
|
+
// ============================================================
|
|
63
|
+
|
|
64
|
+
class ServerWebSocket extends EventTarget {
|
|
65
|
+
|
|
66
|
+
static get CONNECTING() { return CONNECTING; }
|
|
67
|
+
static get OPEN() { return OPEN; }
|
|
68
|
+
static get CLOSING() { return CLOSING; }
|
|
69
|
+
static get CLOSED() { return CLOSED; }
|
|
70
|
+
|
|
71
|
+
constructor(socket, options) {
|
|
72
|
+
super();
|
|
73
|
+
options = options || {};
|
|
74
|
+
|
|
75
|
+
this._socket = socket;
|
|
76
|
+
this._readyState = OPEN; // the handshake is already complete
|
|
77
|
+
this._bufferedAmount = 0;
|
|
78
|
+
this._binaryType = 'blob'; // the browser's default
|
|
79
|
+
this._protocol = options.protocol || '';
|
|
80
|
+
this._extensions = '';
|
|
81
|
+
this._url = options.url || '';
|
|
82
|
+
|
|
83
|
+
this._closeSent = false;
|
|
84
|
+
this._closeReceived = false;
|
|
85
|
+
this._closeCode = CLOSE.ABNORMAL;
|
|
86
|
+
this._closeReason = '';
|
|
87
|
+
this._closeTimer = null;
|
|
88
|
+
|
|
89
|
+
this._onopen = null; this._onmessage = null;
|
|
90
|
+
this._onerror = null; this._onclose = null; this._onpong = null;
|
|
91
|
+
|
|
92
|
+
// Adds .on/.once/.off and the _emitNode helper. See ws.js (installEventSurfaces) for why
|
|
93
|
+
// the two surfaces receive different arguments for the same event.
|
|
94
|
+
installEventSurfaces(this);
|
|
95
|
+
|
|
96
|
+
var self = this;
|
|
97
|
+
// isServer: true — incoming frames MUST be masked and outgoing ones must
|
|
98
|
+
// NOT be. The parser enforces the direction; getting it backwards is a
|
|
99
|
+
// protocol error the peer will close on.
|
|
100
|
+
var parser = new WSParser({ isServer: true, maxPayload: options.maxPayload });
|
|
101
|
+
this._parser = parser;
|
|
102
|
+
|
|
103
|
+
parser.on('message', function (data, isBinary) {
|
|
104
|
+
if (self._readyState !== OPEN) return;
|
|
105
|
+
emitBoth(self, 'message',
|
|
106
|
+
new MessageEvent('message', {
|
|
107
|
+
data: isBinary ? self._toBinary(data) : data,
|
|
108
|
+
origin: self._url
|
|
109
|
+
}),
|
|
110
|
+
// The ws surface gets the raw payload, NOT self._toBinary(data): `ws`
|
|
111
|
+
// has no binaryType and its handlers always receive a Buffer.
|
|
112
|
+
[nodeMessagePayload(data, isBinary), isBinary]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
parser.on('ping', function (payload) {
|
|
116
|
+
// Answered automatically, as a browser does — but `ws` DOES surface
|
|
117
|
+
// pings to its listeners, so the ws-side event fires. The WHATWG side
|
|
118
|
+
// has no 'ping' event at all, which is why this one is emitted on that
|
|
119
|
+
// surface only.
|
|
120
|
+
if (self._readyState === OPEN) self._sendFrame(OPCODE.PONG, payload);
|
|
121
|
+
self._emitNode('ping', [payload]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
parser.on('pong', function (payload) {
|
|
125
|
+
self._pongSeen = true;
|
|
126
|
+
emitBoth(self, 'pong', new MessageEvent('pong', { data: payload }), [payload]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
parser.on('close', function (code, reason) {
|
|
130
|
+
self._closeReceived = true;
|
|
131
|
+
self._closeCode = code;
|
|
132
|
+
// Only take the peer's reason when we did not supply one. When WE
|
|
133
|
+
// started the handshake, the reason belongs to us: a peer that echoes an
|
|
134
|
+
// empty reason (or a different one) must not overwrite what the
|
|
135
|
+
// application asked to report.
|
|
136
|
+
if (!self._closeSent || !self._closeReason) self._closeReason = reason;
|
|
137
|
+
if (!self._closeSent) {
|
|
138
|
+
// Echo the peer's code AND reason back (RFC 6455 §5.5.1 allows any
|
|
139
|
+
// reason; `ws` echoes the one it received, and matching it matters:
|
|
140
|
+
// the peer reports what comes back in ITS close event, so replying
|
|
141
|
+
// with an empty reason silently erases the reason the peer just gave).
|
|
142
|
+
self._readyState = CLOSING;
|
|
143
|
+
self._sendCloseFrame(code === CLOSE.NO_STATUS ? undefined : code,
|
|
144
|
+
Buffer.from(reason || '', 'utf8'));
|
|
145
|
+
}
|
|
146
|
+
self._teardown(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
parser.on('error', function (err) {
|
|
150
|
+
self._sendCloseFrame(err.closeCode || CLOSE.PROTOCOL_ERROR, Buffer.alloc(0));
|
|
151
|
+
self._closeCode = err.closeCode || CLOSE.PROTOCOL_ERROR;
|
|
152
|
+
self._fail(err);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
socket.on('data', function (chunk) { parser.execute(chunk); });
|
|
156
|
+
socket.on('error', function (err) { self._fail(err); });
|
|
157
|
+
socket.on('close', function () { self._teardown(self._closeSent && self._closeReceived); });
|
|
158
|
+
socket.on('end', function () { self._teardown(self._closeSent && self._closeReceived); });
|
|
159
|
+
|
|
160
|
+
if (socket.setNoDelay) socket.setNoDelay(true);
|
|
161
|
+
|
|
162
|
+
// Frames that shared a TCP segment with the handshake request. A client
|
|
163
|
+
// may legitimately send its first frame immediately after the request, and
|
|
164
|
+
// dropping these loses it — a bug that only appears under load, when
|
|
165
|
+
// segments happen to coalesce.
|
|
166
|
+
//
|
|
167
|
+
// Deferred, and that is not a detail. The application attaches onmessage
|
|
168
|
+
// from the connection handler, which cannot run until this constructor
|
|
169
|
+
// returns — so feeding the head here would emit a 'message' with nobody
|
|
170
|
+
// listening and the frame would be silently swallowed. The failure looks
|
|
171
|
+
// exactly like the bug this code exists to prevent: the first frame goes
|
|
172
|
+
// missing, only when segments coalesce.
|
|
173
|
+
// A server-side socket is open the moment it exists, but `ws` code written
|
|
174
|
+
// for the client side still waits for 'open'. Fire it once, asynchronously,
|
|
175
|
+
// so a handler attached right after construction sees it.
|
|
176
|
+
queueMicrotask(function () {
|
|
177
|
+
if (self._readyState === OPEN) emitBoth(self, 'open', new Event('open'), []);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (options.head && options.head.length) {
|
|
181
|
+
var head = options.head;
|
|
182
|
+
queueMicrotask(function () {
|
|
183
|
+
if (self._readyState === OPEN) parser.execute(head);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get url() { return this._url; }
|
|
189
|
+
get readyState() { return this._readyState; }
|
|
190
|
+
get bufferedAmount() { return this._bufferedAmount; }
|
|
191
|
+
get protocol() { return this._protocol; }
|
|
192
|
+
get extensions() { return this._extensions; }
|
|
193
|
+
|
|
194
|
+
get CONNECTING() { return CONNECTING; }
|
|
195
|
+
get OPEN() { return OPEN; }
|
|
196
|
+
get CLOSING() { return CLOSING; }
|
|
197
|
+
get CLOSED() { return CLOSED; }
|
|
198
|
+
|
|
199
|
+
get binaryType() { return this._binaryType; }
|
|
200
|
+
set binaryType(v) {
|
|
201
|
+
if (v !== 'blob' && v !== 'arraybuffer' && v !== 'nodebuffer') return;
|
|
202
|
+
this._binaryType = v;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get onopen() { return this._onopen; }
|
|
206
|
+
set onopen(fn) { this._setHandler('open', '_onopen', fn); }
|
|
207
|
+
get onmessage() { return this._onmessage; }
|
|
208
|
+
set onmessage(fn) { this._setHandler('message', '_onmessage', fn); }
|
|
209
|
+
get onerror() { return this._onerror; }
|
|
210
|
+
set onerror(fn) { this._setHandler('error', '_onerror', fn); }
|
|
211
|
+
get onclose() { return this._onclose; }
|
|
212
|
+
set onclose(fn) { this._setHandler('close', '_onclose', fn); }
|
|
213
|
+
/** Not in the browser — see ping() below. */
|
|
214
|
+
get onpong() { return this._onpong; }
|
|
215
|
+
set onpong(fn) { this._setHandler('pong', '_onpong', fn); }
|
|
216
|
+
|
|
217
|
+
_setHandler(type, slot, fn) {
|
|
218
|
+
if (this[slot]) this.removeEventListener(type, this[slot]);
|
|
219
|
+
this[slot] = typeof fn === 'function' ? fn : null;
|
|
220
|
+
if (this[slot]) this.addEventListener(type, this[slot]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* send(data[, options])
|
|
225
|
+
*
|
|
226
|
+
* `options` is the ws extension, not WHATWG — a browser's send() takes one
|
|
227
|
+
* argument and infers text-vs-binary from the type. That inference is wrong
|
|
228
|
+
* for the ws surface, where every message arrives as a Buffer: echoing one
|
|
229
|
+
* back would turn a text message into a binary one, and the peer would
|
|
230
|
+
* report isBinary: true for something the sender called text.
|
|
231
|
+
*
|
|
232
|
+
* So `{ binary: false }` forces a Buffer to go out as a text frame, and
|
|
233
|
+
* `{ binary: true }` forces a string to go out as binary. Omitted, the
|
|
234
|
+
* browser's inference applies and browser code is unaffected.
|
|
235
|
+
*/
|
|
236
|
+
send(data, options) {
|
|
237
|
+
if (this._readyState === CONNECTING) {
|
|
238
|
+
throw new DOMException('Still in CONNECTING state.', 'InvalidStateError');
|
|
239
|
+
}
|
|
240
|
+
if (this._readyState !== OPEN) return;
|
|
241
|
+
|
|
242
|
+
var self = this;
|
|
243
|
+
var forced = options && typeof options.binary === 'boolean' ? options.binary : null;
|
|
244
|
+
|
|
245
|
+
if (typeof data === 'string') {
|
|
246
|
+
this._sendFrame(forced === true ? OPCODE.BINARY : OPCODE.TEXT,
|
|
247
|
+
Buffer.from(data, 'utf8'));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (data instanceof Blob) {
|
|
251
|
+
this._bufferedAmount += data.size;
|
|
252
|
+
var pending = data.size;
|
|
253
|
+
data.arrayBuffer().then(function (ab) {
|
|
254
|
+
self._bufferedAmount -= pending;
|
|
255
|
+
if (self._readyState === OPEN) {
|
|
256
|
+
self._sendFrame(forced === false ? OPCODE.TEXT : OPCODE.BINARY, Buffer.from(ab));
|
|
257
|
+
}
|
|
258
|
+
}, function () { self._bufferedAmount -= pending; });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (ArrayBuffer.isView(data)) {
|
|
262
|
+
this._sendFrame(forced === false ? OPCODE.TEXT : OPCODE.BINARY,
|
|
263
|
+
Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (data instanceof ArrayBuffer) {
|
|
267
|
+
this._sendFrame(forced === false ? OPCODE.TEXT : OPCODE.BINARY, Buffer.from(data));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this._sendFrame(OPCODE.TEXT, Buffer.from(String(data), 'utf8'));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Not in the browser, and a server cannot do without it.
|
|
275
|
+
*
|
|
276
|
+
* A browser answers pings itself and its connections are supervised by the
|
|
277
|
+
* page lifecycle. A server holding thousands of long-lived sockets has no
|
|
278
|
+
* such supervision: a peer that vanishes without a FIN — a laptop lid, a NAT
|
|
279
|
+
* timeout — leaves a socket that looks perfectly healthy for many minutes.
|
|
280
|
+
* A ping with no pong is the only way to notice.
|
|
281
|
+
*/
|
|
282
|
+
ping(data) {
|
|
283
|
+
if (this._readyState !== OPEN) return;
|
|
284
|
+
this._pongSeen = false;
|
|
285
|
+
this._sendFrame(OPCODE.PING, data == null ? Buffer.alloc(0)
|
|
286
|
+
: Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_sendFrame(opcode, payload) {
|
|
290
|
+
if (!this._socket || this._socket.destroyed) return;
|
|
291
|
+
var frame;
|
|
292
|
+
// mask: false — a SERVER must never mask (RFC 6455 §5.1). This is the one
|
|
293
|
+
// line that differs from the client, and a client will close the
|
|
294
|
+
// connection with 1002 if it is wrong.
|
|
295
|
+
try { frame = encodeFrame(opcode, payload, { mask: false }); }
|
|
296
|
+
catch (e) { this._fail(e); return; }
|
|
297
|
+
|
|
298
|
+
var self = this;
|
|
299
|
+
if (opcode < 0x8) this._bufferedAmount += payload.length;
|
|
300
|
+
this._socket.write(frame, function () {
|
|
301
|
+
if (opcode < 0x8) self._bufferedAmount -= payload.length;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
close(code, reason) {
|
|
306
|
+
if (code !== undefined && code !== null) {
|
|
307
|
+
code = Number(code);
|
|
308
|
+
if (!isValidUserCloseCode(code)) {
|
|
309
|
+
throw new DOMException('The close code must be 1000 or between 3000 and 4999.',
|
|
310
|
+
'InvalidAccessError');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
var reasonBuf = Buffer.alloc(0);
|
|
314
|
+
if (reason !== undefined && reason !== null) {
|
|
315
|
+
reasonBuf = Buffer.from(String(reason), 'utf8');
|
|
316
|
+
if (reasonBuf.length > 123) {
|
|
317
|
+
throw new DOMException('The close reason must not be greater than 123 UTF-8 bytes.',
|
|
318
|
+
'SyntaxError');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (this._readyState === CLOSING || this._readyState === CLOSED) return;
|
|
322
|
+
|
|
323
|
+
this._readyState = CLOSING;
|
|
324
|
+
this._closeCode = code === undefined ? CLOSE.NORMAL : code;
|
|
325
|
+
this._closeReason = reason ? String(reason) : '';
|
|
326
|
+
this._sendCloseFrame(code === undefined ? undefined : code, reasonBuf);
|
|
327
|
+
|
|
328
|
+
var self = this;
|
|
329
|
+
this._closeTimer = setTimeout(function () { self._teardown(false); }, CLOSE_TIMEOUT);
|
|
330
|
+
if (this._closeTimer.unref) this._closeTimer.unref();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_sendCloseFrame(code, reasonBuf) {
|
|
334
|
+
if (this._closeSent) return;
|
|
335
|
+
this._closeSent = true;
|
|
336
|
+
var payload;
|
|
337
|
+
if (code === undefined) payload = Buffer.alloc(0);
|
|
338
|
+
else {
|
|
339
|
+
payload = Buffer.alloc(2 + (reasonBuf ? reasonBuf.length : 0));
|
|
340
|
+
payload.writeUInt16BE(code, 0);
|
|
341
|
+
if (reasonBuf && reasonBuf.length) reasonBuf.copy(payload, 2);
|
|
342
|
+
}
|
|
343
|
+
if (this._socket && !this._socket.destroyed) {
|
|
344
|
+
try { this._socket.write(encodeFrame(OPCODE.CLOSE, payload, { mask: false })); } catch (e) {}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_toBinary(buf) {
|
|
349
|
+
if (this._binaryType === 'nodebuffer') return buf;
|
|
350
|
+
if (this._binaryType === 'arraybuffer') {
|
|
351
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
352
|
+
}
|
|
353
|
+
return new Blob([buf]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_fail(err) {
|
|
357
|
+
if (this._readyState === CLOSED) return;
|
|
358
|
+
var event = new Event('error');
|
|
359
|
+
try { Object.defineProperty(event, 'error', { value: err, enumerable: true }); } catch (e) {}
|
|
360
|
+
try { Object.defineProperty(event, 'message', { value: err && err.message, enumerable: true }); } catch (e) {}
|
|
361
|
+
// WHATWG gets an Event; ws gets the Error itself.
|
|
362
|
+
emitBoth(this, 'error', event, [err]);
|
|
363
|
+
this._teardown(false);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
_teardown(wasClean) {
|
|
367
|
+
if (this._readyState === CLOSED) return;
|
|
368
|
+
this._readyState = CLOSED;
|
|
369
|
+
if (this._closeTimer) { clearTimeout(this._closeTimer); this._closeTimer = null; }
|
|
370
|
+
if (this._socket) {
|
|
371
|
+
var s = this._socket;
|
|
372
|
+
this._socket = null;
|
|
373
|
+
try { s.removeAllListeners('data'); } catch (e) {}
|
|
374
|
+
try { s.destroy(); } catch (e) {}
|
|
375
|
+
}
|
|
376
|
+
var code = wasClean ? this._closeCode : (this._closeReceived ? this._closeCode : CLOSE.ABNORMAL);
|
|
377
|
+
var reason = this._closeReason || '';
|
|
378
|
+
emitBoth(this, 'close',
|
|
379
|
+
new CloseEventCtor('close', { code: code, reason: reason, wasClean: !!wasClean }),
|
|
380
|
+
// `ws` passes the reason as a Buffer, not a string. Kept as it is:
|
|
381
|
+
// handlers written against ws call .toString() on it.
|
|
382
|
+
[code, Buffer.from(reason, 'utf8')]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
// ============================================================
|
|
388
|
+
// Handshake
|
|
389
|
+
// ============================================================
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Refuse an upgrade with a real HTTP response.
|
|
393
|
+
*
|
|
394
|
+
* Worth doing rather than destroying the socket: a client that gets a 400 with
|
|
395
|
+
* a reason can be fixed, while one that gets a connection reset cannot tell a
|
|
396
|
+
* rejected handshake from a network fault. Modelled on how the `ws` package
|
|
397
|
+
* handles this.
|
|
398
|
+
*/
|
|
399
|
+
function abortHandshake(socket, code, message, extraHeaders) {
|
|
400
|
+
message = message || STATUS_TEXT[code] || '';
|
|
401
|
+
var headers = {
|
|
402
|
+
'Connection': 'close',
|
|
403
|
+
'Content-Type': 'text/plain',
|
|
404
|
+
'Content-Length': String(Buffer.byteLength(message))
|
|
405
|
+
};
|
|
406
|
+
for (var h in (extraHeaders || {})) headers[h] = extraHeaders[h];
|
|
407
|
+
|
|
408
|
+
var lines = ['HTTP/1.1 ' + code + ' ' + (STATUS_TEXT[code] || '')];
|
|
409
|
+
for (var name in headers) lines.push(name + ': ' + headers[name]);
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
socket.once('finish', function () { try { socket.destroy(); } catch (e) {} });
|
|
413
|
+
socket.end(lines.join('\r\n') + '\r\n\r\n' + message);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
try { socket.destroy(); } catch (e2) {}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Refuse an upgrade on whichever transport it arrived over.
|
|
422
|
+
*
|
|
423
|
+
* HTTP/1.1 gets a status line on the raw socket; HTTP/2 gets a response on the
|
|
424
|
+
* stream. Same intent, and the caller should not have to care which — the only
|
|
425
|
+
* reason this function exists is that the two cannot share bytes.
|
|
426
|
+
*/
|
|
427
|
+
function rejectUpgrade(socket, isH2, code, message, extraHeaders) {
|
|
428
|
+
if (isH2 && typeof socket._h2Reject === 'function') {
|
|
429
|
+
socket._h2Reject(code, message);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
abortHandshake(socket, code, message, extraHeaders);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
/** Parse Sec-WebSocket-Protocol into an ordered list, rejecting duplicates. */
|
|
437
|
+
function parseSubprotocols(header) {
|
|
438
|
+
var out = [];
|
|
439
|
+
var seen = Object.create(null);
|
|
440
|
+
var parts = String(header).split(',');
|
|
441
|
+
for (var i = 0; i < parts.length; i++) {
|
|
442
|
+
var p = parts[i].trim();
|
|
443
|
+
if (!isToken(p)) throw new Error('invalid subprotocol: ' + JSON.stringify(p));
|
|
444
|
+
if (seen[p]) throw new Error('duplicate subprotocol: ' + p);
|
|
445
|
+
seen[p] = true;
|
|
446
|
+
out.push(p);
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Complete a WebSocket handshake on an upgraded socket and return the
|
|
454
|
+
* WebSocket.
|
|
455
|
+
*
|
|
456
|
+
* The primitive: `server.on('upgrade', (req, socket, head) => ...)` gives all
|
|
457
|
+
* three arguments this needs, so an application can accept a WebSocket
|
|
458
|
+
* anywhere it can see an upgrade, without a WebSocketServer at all.
|
|
459
|
+
*
|
|
460
|
+
* Returns null and answers the client itself if the handshake is invalid.
|
|
461
|
+
*/
|
|
462
|
+
function upgradeWebSocket(req, socket, head, options) {
|
|
463
|
+
options = options || {};
|
|
464
|
+
|
|
465
|
+
var headers = req.headers || {};
|
|
466
|
+
var version = parseInt(headers['sec-websocket-version'], 10);
|
|
467
|
+
|
|
468
|
+
// ---- Which transport is this? ----
|
|
469
|
+
//
|
|
470
|
+
// Over HTTP/1.1 a WebSocket arrives as an Upgrade: a GET with
|
|
471
|
+
// Sec-WebSocket-Key, answered with 101 and a computed Accept.
|
|
472
|
+
//
|
|
473
|
+
// Over HTTP/2 it arrives as extended CONNECT (RFC 8441): a CONNECT with
|
|
474
|
+
// `:protocol: websocket`, answered with 200 — and with NO Sec-WebSocket-Key
|
|
475
|
+
// and no Accept at all. §5 removes them deliberately: the key exists only to
|
|
476
|
+
// stop a naive HTTP/1.1 server being tricked into completing a handshake by
|
|
477
|
+
// echoing a header, and HTTP/2 has no such confusion to prevent because the
|
|
478
|
+
// stream is already framed.
|
|
479
|
+
//
|
|
480
|
+
// Everything after this point — subprotocols, origin, and every frame — is
|
|
481
|
+
// identical, which is why the two transports share this function rather than
|
|
482
|
+
// having one each.
|
|
483
|
+
var isH2 = typeof socket._h2Accept === 'function';
|
|
484
|
+
var key = headers['sec-websocket-key'];
|
|
485
|
+
var upgrade = headers['upgrade'];
|
|
486
|
+
|
|
487
|
+
if (isH2) {
|
|
488
|
+
if (String(headers[':protocol'] || '').toLowerCase() !== 'websocket') {
|
|
489
|
+
rejectUpgrade(socket, isH2, 400, 'Extended CONNECT is not for websocket');
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
// Each of these is checked before any state is built, and each answers with
|
|
494
|
+
// the status RFC 6455 §4.2.1 calls for.
|
|
495
|
+
if (req.method !== 'GET') {
|
|
496
|
+
abortHandshake(socket, 405, 'Invalid HTTP method');
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
if (upgrade === undefined || String(upgrade).toLowerCase() !== 'websocket') {
|
|
500
|
+
abortHandshake(socket, 400, 'Invalid Upgrade header');
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
if (key === undefined || !KEY_RE.test(String(key))) {
|
|
504
|
+
// 16 random bytes in base64 is always 24 chars ending '=='. A client
|
|
505
|
+
// that sends anything else is not really speaking WebSocket.
|
|
506
|
+
abortHandshake(socket, 400, 'Missing or invalid Sec-WebSocket-Key header');
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Over HTTP/1.1 the header is mandatory: it is the only thing that
|
|
512
|
+
// distinguishes a WebSocket handshake from an ordinary Upgrade, and a
|
|
513
|
+
// request without it is not one.
|
|
514
|
+
//
|
|
515
|
+
// Over HTTP/2 and HTTP/3, `:protocol: websocket` has already said so
|
|
516
|
+
// unambiguously, and browsers do not reliably send Sec-WebSocket-Version on
|
|
517
|
+
// the extended CONNECT — Chrome's own network log of a WebSocket request
|
|
518
|
+
// shows five pseudo-headers and nothing else. RFC 8441 §5 asks for it, but
|
|
519
|
+
// refusing over it costs far more than it protects: the browser gets a 400,
|
|
520
|
+
// silently abandons HTTP/2, and reopens the WebSocket over HTTP/1.1. The
|
|
521
|
+
// connection still works, so nothing looks broken — it just never uses the
|
|
522
|
+
// multiplexed path, and the reason is invisible from both ends.
|
|
523
|
+
//
|
|
524
|
+
// So it is required where it carries meaning and merely validated where it
|
|
525
|
+
// does not: present and wrong is still an error, absent over h2/h3 is not.
|
|
526
|
+
if (isH2) {
|
|
527
|
+
if (headers['sec-websocket-version'] !== undefined && version !== 13) {
|
|
528
|
+
rejectUpgrade(socket, isH2, 400, 'Unsupported Sec-WebSocket-Version',
|
|
529
|
+
{ 'Sec-WebSocket-Version': '13' });
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
} else if (version !== 13) {
|
|
533
|
+
// Only version 13 exists. The header tells the client which we accept
|
|
534
|
+
// (RFC 6455 §4.2.2).
|
|
535
|
+
abortHandshake(socket, 400, 'Unsupported Sec-WebSocket-Version',
|
|
536
|
+
{ 'Sec-WebSocket-Version': '13' });
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
var offered = [];
|
|
541
|
+
if (headers['sec-websocket-protocol'] !== undefined) {
|
|
542
|
+
try {
|
|
543
|
+
offered = parseSubprotocols(headers['sec-websocket-protocol']);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
rejectUpgrade(socket, isH2, 400, 'Invalid Sec-WebSocket-Protocol header');
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ---- Origin ----
|
|
551
|
+
//
|
|
552
|
+
// The same-origin policy does NOT apply to WebSocket: a browser will happily
|
|
553
|
+
// open one from any page to any host, and it sends the user's cookies with
|
|
554
|
+
// the handshake. Without an origin check, any site the user visits can talk
|
|
555
|
+
// to this server as that user — "cross-site WebSocket hijacking".
|
|
556
|
+
//
|
|
557
|
+
// So the check is opt-in but the default is a refusal to guess: with no
|
|
558
|
+
// `origin` option nothing is enforced (a server behind a gateway, or one
|
|
559
|
+
// that authenticates per message, is entitled to that), and the moment an
|
|
560
|
+
// allowlist is given it is enforced strictly.
|
|
561
|
+
if (options.origin !== undefined) {
|
|
562
|
+
var origin = headers['origin'];
|
|
563
|
+
var allowed = options.origin;
|
|
564
|
+
var ok;
|
|
565
|
+
if (typeof allowed === 'function') ok = !!allowed(origin, req);
|
|
566
|
+
else if (Array.isArray(allowed)) ok = allowed.indexOf(origin) !== -1;
|
|
567
|
+
else ok = (origin === allowed);
|
|
568
|
+
if (!ok) {
|
|
569
|
+
rejectUpgrade(socket, isH2, 403, 'Origin not allowed');
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (typeof options.verifyClient === 'function') {
|
|
575
|
+
var verdict = options.verifyClient(req);
|
|
576
|
+
if (verdict === false) { rejectUpgrade(socket, isH2, 401, 'Unauthorized'); return null; }
|
|
577
|
+
if (verdict && verdict.code) {
|
|
578
|
+
rejectUpgrade(socket, isH2, verdict.code, verdict.message, verdict.headers);
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// A server may only select a protocol the client offered (§4.2.2).
|
|
584
|
+
var chosen = null;
|
|
585
|
+
if (options.protocol !== undefined && options.protocol !== null) {
|
|
586
|
+
var want = typeof options.protocol === 'function'
|
|
587
|
+
? options.protocol(offered, req)
|
|
588
|
+
: options.protocol;
|
|
589
|
+
if (want) {
|
|
590
|
+
if (offered.indexOf(want) === -1) {
|
|
591
|
+
rejectUpgrade(socket, isH2, 400, 'Requested subprotocol was not offered by the client');
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
chosen = want;
|
|
595
|
+
}
|
|
596
|
+
} else if (options.protocols && offered.length) {
|
|
597
|
+
for (var i = 0; i < options.protocols.length && chosen === null; i++) {
|
|
598
|
+
if (offered.indexOf(options.protocols[i]) !== -1) chosen = options.protocols[i];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
if (isH2) {
|
|
604
|
+
// §5: a 2xx accepts the channel. No 101, no Accept — see the note above.
|
|
605
|
+
var h2Headers = {};
|
|
606
|
+
if (chosen) h2Headers['sec-websocket-protocol'] = chosen;
|
|
607
|
+
for (var hn2 in (options.headers || {})) h2Headers[hn2] = options.headers[hn2];
|
|
608
|
+
socket._h2Accept(h2Headers);
|
|
609
|
+
} else {
|
|
610
|
+
var responseLines = [
|
|
611
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
612
|
+
'Upgrade: websocket',
|
|
613
|
+
'Connection: Upgrade',
|
|
614
|
+
// The one value that proves we are a WebSocket server and not an HTTP
|
|
615
|
+
// server tricked into echoing a header (§1.3).
|
|
616
|
+
'Sec-WebSocket-Accept: ' + acceptKey(String(key))
|
|
617
|
+
];
|
|
618
|
+
if (chosen) responseLines.push('Sec-WebSocket-Protocol: ' + chosen);
|
|
619
|
+
for (var hn in (options.headers || {})) responseLines.push(hn + ': ' + options.headers[hn]);
|
|
620
|
+
socket.write(responseLines.join('\r\n') + '\r\n\r\n');
|
|
621
|
+
}
|
|
622
|
+
} catch (err) {
|
|
623
|
+
try { socket.destroy(); } catch (e) {}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
var scheme = (socket.encrypted || req.socket && req.socket.encrypted) ? 'wss' : 'ws';
|
|
628
|
+
return new ServerWebSocket(socket, {
|
|
629
|
+
protocol: chosen || '',
|
|
630
|
+
head: head,
|
|
631
|
+
maxPayload: options.maxPayload,
|
|
632
|
+
// The origin the client asked for: Host over HTTP/1.1, :authority over
|
|
633
|
+
// HTTP/2 and HTTP/3 (RFC 9113 §8.3.1, RFC 9114 §4.3.1) — over HTTP/3 the
|
|
634
|
+
// request carries no Host at all, and the URL came out as "wss:///path".
|
|
635
|
+
url: scheme + '://' + (headers['host'] || headers[':authority'] || '') + (req.url || '')
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
// ============================================================
|
|
641
|
+
// WebSocketServer
|
|
642
|
+
// ============================================================
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Attach to a quico (or node:http) server and hand out WebSockets.
|
|
646
|
+
*
|
|
647
|
+
* const wss = new WebSocketServer({ server, path: '/chat' });
|
|
648
|
+
* wss.onconnection = (ws, req) => {
|
|
649
|
+
* ws.onmessage = e => ws.send(e.data);
|
|
650
|
+
* };
|
|
651
|
+
*
|
|
652
|
+
* `onconnection` rather than on('connection'): if the connection object is the
|
|
653
|
+
* browser's WebSocket, the surface around it should read the same way.
|
|
654
|
+
* addEventListener works too, for the same reason.
|
|
655
|
+
*/
|
|
656
|
+
class WebSocketServer extends EventTarget {
|
|
657
|
+
|
|
658
|
+
constructor(options) {
|
|
659
|
+
super();
|
|
660
|
+
options = options || {};
|
|
661
|
+
this.options = options;
|
|
662
|
+
this.clients = new Set();
|
|
663
|
+
this._onconnection = null;
|
|
664
|
+
this._pingTimer = null;
|
|
665
|
+
|
|
666
|
+
// The server object gets both surfaces as well: wss.onconnection is the
|
|
667
|
+
// browser-style spelling, wss.on('connection') the ws one, and both
|
|
668
|
+
// receive the same two arguments — there is no payload disagreement here
|
|
669
|
+
// to preserve.
|
|
670
|
+
installEventSurfaces(this);
|
|
671
|
+
|
|
672
|
+
// `noServer: true` is the ws spelling of "I will call handleUpgrade
|
|
673
|
+
// myself". Nothing to do for it here — not passing a `server` already has
|
|
674
|
+
// that effect — but accepting the option means ws code reads the same.
|
|
675
|
+
if (options.server && !options.noServer) {
|
|
676
|
+
var self = this;
|
|
677
|
+
options.server.on('upgrade', function (req, socket, head) {
|
|
678
|
+
self.handleUpgrade(req, socket, head);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Keepalive. Off unless asked for, because it costs a frame per socket per
|
|
683
|
+
// interval — but a long-lived server almost always wants it: see ping().
|
|
684
|
+
if (options.pingInterval > 0) this.startAutoPing(options.pingInterval);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
get onconnection() { return this._onconnection; }
|
|
688
|
+
set onconnection(fn) {
|
|
689
|
+
if (this._onconnection) this.removeEventListener('connection', this._onconnection);
|
|
690
|
+
this._onconnection = typeof fn === 'function' ? fn : null;
|
|
691
|
+
if (this._onconnection) this.addEventListener('connection', this._onconnection);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Accept (or refuse) one upgrade. Public so an application can route
|
|
696
|
+
* upgrades itself and hand only some of them here.
|
|
697
|
+
*/
|
|
698
|
+
/**
|
|
699
|
+
* Accept (or refuse) one upgrade.
|
|
700
|
+
*
|
|
701
|
+
* `cb` is the ws-compatible form: `handleUpgrade(req, socket, head, ws => …)`
|
|
702
|
+
* hands the socket to the callback and does NOT fire the connection event,
|
|
703
|
+
* because that is what `ws` does — with noServer the callback is the whole
|
|
704
|
+
* delivery, and code written against it emits 'connection' itself if it
|
|
705
|
+
* wants to. Omit the callback and the connection event fires as usual.
|
|
706
|
+
*/
|
|
707
|
+
handleUpgrade(req, socket, head, cb) {
|
|
708
|
+
if (this.options.path !== undefined) {
|
|
709
|
+
var path = String(req.url || '').split('?')[0];
|
|
710
|
+
if (path !== this.options.path) {
|
|
711
|
+
// Not ours. Another WebSocketServer on the same server may want it, so
|
|
712
|
+
// do not refuse on its behalf — but do not simply walk away either:
|
|
713
|
+
// the socket has been handed over by the HTTP layer and nothing else
|
|
714
|
+
// owns it, so an unclaimed upgrade hangs until the client times out.
|
|
715
|
+
//
|
|
716
|
+
// Refusing after a turn of the event loop gives any other handler
|
|
717
|
+
// registered on the same 'upgrade' event a chance to claim it first;
|
|
718
|
+
// if none did, the client gets a 404 and knows immediately.
|
|
719
|
+
var isH2Stream = typeof socket._h2Reject === 'function';
|
|
720
|
+
queueMicrotask(function () {
|
|
721
|
+
if (socket.destroyed || socket._quicoWsClaimed) return;
|
|
722
|
+
// Transport-aware, for the same reason every other refusal is: over
|
|
723
|
+
// HTTP/2 there is no socket to write a status line onto, and writing
|
|
724
|
+
// one would put HTTP/1.1 bytes into a DATA frame. The h2 path
|
|
725
|
+
// answers on the stream instead.
|
|
726
|
+
rejectUpgrade(socket, isH2Stream, 404, 'No WebSocket endpoint at ' + path);
|
|
727
|
+
});
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
socket._quicoWsClaimed = true;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
var ws = upgradeWebSocket(req, socket, head, this.options);
|
|
734
|
+
if (!ws) return false;
|
|
735
|
+
|
|
736
|
+
var self = this;
|
|
737
|
+
this.clients.add(ws);
|
|
738
|
+
ws.addEventListener('close', function () { self.clients.delete(ws); });
|
|
739
|
+
|
|
740
|
+
if (typeof cb === 'function') { cb(ws, req); return true; }
|
|
741
|
+
|
|
742
|
+
// dispatchEvent carries one argument and a connection handler needs two —
|
|
743
|
+
// the socket and the request that produced it — so the handler is called
|
|
744
|
+
// directly, and the Event is dispatched as well (carrying both on itself)
|
|
745
|
+
// so addEventListener still sees the connection.
|
|
746
|
+
if (this._onconnection) this._onconnection(ws, req);
|
|
747
|
+
|
|
748
|
+
var ev = new Event('connection');
|
|
749
|
+
try {
|
|
750
|
+
Object.defineProperty(ev, 'webSocket', { value: ws, enumerable: true });
|
|
751
|
+
Object.defineProperty(ev, 'request', { value: req, enumerable: true });
|
|
752
|
+
} catch (e) {}
|
|
753
|
+
var handler = this._onconnection;
|
|
754
|
+
if (handler) this.removeEventListener('connection', handler);
|
|
755
|
+
this.dispatchEvent(ev);
|
|
756
|
+
if (handler) this.addEventListener('connection', handler);
|
|
757
|
+
|
|
758
|
+
// ws-style listeners get (ws, req), the signature every ws example uses.
|
|
759
|
+
this._emitNode('connection', [ws, req]);
|
|
760
|
+
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Ping every client on an interval and drop the ones that stop answering.
|
|
766
|
+
*
|
|
767
|
+
* The reason a server needs this and a browser does not: a peer that
|
|
768
|
+
* disappears without a FIN — a closed laptop, a NAT that forgot the mapping
|
|
769
|
+
* — leaves a socket that reads as healthy indefinitely. Nothing else
|
|
770
|
+
* notices.
|
|
771
|
+
*/
|
|
772
|
+
startAutoPing(intervalMs) {
|
|
773
|
+
var self = this;
|
|
774
|
+
this.stopAutoPing();
|
|
775
|
+
this._pingTimer = setInterval(function () {
|
|
776
|
+
for (var ws of self.clients) {
|
|
777
|
+
if (ws.readyState !== OPEN) continue;
|
|
778
|
+
if (ws._awaitingPong && !ws._pongSeen) {
|
|
779
|
+
// Missed the previous round: the peer is gone.
|
|
780
|
+
ws._fail(new Error('no pong received'));
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
ws._awaitingPong = true;
|
|
784
|
+
ws.ping();
|
|
785
|
+
}
|
|
786
|
+
}, intervalMs);
|
|
787
|
+
if (this._pingTimer.unref) this._pingTimer.unref();
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
stopAutoPing() {
|
|
791
|
+
if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Close every client, then stop.
|
|
796
|
+
*
|
|
797
|
+
* 1001 "going away" is exactly the right code for a server shutting down —
|
|
798
|
+
* and the WHATWG close() refuses it, because the browser reserves 1001 for
|
|
799
|
+
* the browser itself (a tab being closed) and only lets a page pass 1000 or
|
|
800
|
+
* 3000-4999. The server is not a page, and RFC 6455 §7.4.1 defines 1001 for
|
|
801
|
+
* precisely this situation, so it goes out through the internal path rather
|
|
802
|
+
* than through the browser-facing method.
|
|
803
|
+
*
|
|
804
|
+
* The first version called ws.close(1001) inside a try/catch and swallowed
|
|
805
|
+
* the DOMException it raised on every client: close() did nothing at all,
|
|
806
|
+
* silently, and the sockets stayed open.
|
|
807
|
+
*/
|
|
808
|
+
close(code, reason) {
|
|
809
|
+
this.stopAutoPing();
|
|
810
|
+
var reasonBuf = reason ? Buffer.from(String(reason), 'utf8') : Buffer.alloc(0);
|
|
811
|
+
if (reasonBuf.length > 123) reasonBuf = reasonBuf.subarray(0, 123);
|
|
812
|
+
var finalCode = code === undefined ? 1001 : code;
|
|
813
|
+
|
|
814
|
+
for (var ws of this.clients) {
|
|
815
|
+
if (ws.readyState !== OPEN) continue;
|
|
816
|
+
ws._readyState = CLOSING;
|
|
817
|
+
ws._closeCode = finalCode;
|
|
818
|
+
ws._closeReason = reasonBuf.toString('utf8');
|
|
819
|
+
ws._sendCloseFrame(finalCode, reasonBuf);
|
|
820
|
+
|
|
821
|
+
// The peer should echo the close frame; if it does not, do not hold the
|
|
822
|
+
// socket open forever on a server that is shutting down.
|
|
823
|
+
(function (sock) {
|
|
824
|
+
var t = setTimeout(function () { sock._teardown(false); }, 5000);
|
|
825
|
+
if (t.unref) t.unref();
|
|
826
|
+
sock._closeTimer = t;
|
|
827
|
+
})(ws);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
export { WebSocketServer, ServerWebSocket, upgradeWebSocket, abortHandshake };
|