quico 0.7.0 → 0.9.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 +247 -33
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +9 -5
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +10 -0
- package/src/crypto.js +6 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +115 -70
- package/src/h3_client.js +25 -5
- package/src/h3_server.js +104 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +379 -63
- package/src/quic_server.js +213 -72
- package/src/quic_socket.js +5 -5
- package/src/server.js +277 -24
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +62 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +432 -57
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
package/src/ws.js
CHANGED
|
@@ -606,7 +606,127 @@ function nodeMessagePayload(data, isBinary) {
|
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
|
|
609
|
+
// ============================================================
|
|
610
|
+
// What the browser's WebSocket and WebSocketStream share
|
|
611
|
+
// ============================================================
|
|
612
|
+
|
|
613
|
+
// close(code)'s code, as WebIDL's [Clamp] unsigned short takes it — a number
|
|
614
|
+
// (NaN, a string, null: 0), clamped to 0..65535, rounded half to even — then
|
|
615
|
+
// 1000 or 3000..4999, or an InvalidAccessError. null was passed over as if
|
|
616
|
+
// no code had been given; the browser throws.
|
|
617
|
+
function userCloseCode(code) {
|
|
618
|
+
var n = Number(code);
|
|
619
|
+
if (isNaN(n)) n = 0;
|
|
620
|
+
n = Math.min(Math.max(n, 0), 65535);
|
|
621
|
+
var f = Math.floor(n), d = n - f;
|
|
622
|
+
n = d > 0.5 || (d === 0.5 && f % 2 === 1) ? f + 1 : f;
|
|
623
|
+
if (!isValidUserCloseCode(n)) {
|
|
624
|
+
throw new DOMException('The close code must be 1000 or between 3000 and 4999.', 'InvalidAccessError');
|
|
625
|
+
}
|
|
626
|
+
return n;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// A close reason, as the browser checks it: at most 123 bytes of UTF-8 (a
|
|
630
|
+
// close frame's 125, less the code), or a SyntaxError. Lone surrogates become
|
|
631
|
+
// U+FFFD, as USVString has them.
|
|
632
|
+
function closeReasonBytes(reason) {
|
|
633
|
+
var buf = Buffer.from(String(reason), 'utf8');
|
|
634
|
+
if (buf.length > 123) {
|
|
635
|
+
throw new DOMException('The close reason must not be greater than 123 UTF-8 bytes.', 'SyntaxError');
|
|
636
|
+
}
|
|
637
|
+
return buf;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The subprotocols a client offers, as the browser checks them: a string or
|
|
641
|
+
// a list of them (WebSocketStream: a list only — a string is a TypeError);
|
|
642
|
+
// each an HTTP token, none twice — compared without regard to case, as the
|
|
643
|
+
// header's values are — or a SyntaxError.
|
|
644
|
+
function subprotocolList(protocols, listOnly) {
|
|
645
|
+
if (protocols === undefined || protocols === null && listOnly) return [];
|
|
646
|
+
if (typeof protocols === 'string') {
|
|
647
|
+
if (listOnly) throw new TypeError('protocols must be a sequence of strings');
|
|
648
|
+
protocols = [protocols];
|
|
649
|
+
} else if (protocols === null || typeof protocols !== 'object' || typeof protocols[Symbol.iterator] !== 'function') {
|
|
650
|
+
if (listOnly) throw new TypeError('protocols must be a sequence of strings');
|
|
651
|
+
protocols = [String(protocols)]; // WebIDL: (DOMString or sequence<DOMString>)
|
|
652
|
+
} else {
|
|
653
|
+
protocols = Array.from(protocols, String);
|
|
654
|
+
}
|
|
655
|
+
var seen = Object.create(null);
|
|
656
|
+
for (var i = 0; i < protocols.length; i++) {
|
|
657
|
+
var p = protocols[i];
|
|
658
|
+
if (p.length === 0 || /[^\x21\x23-\x27\x2a\x2b\x2d\x2e0-9\x41-\x5a\x5e-\x7a\x7c\x7e]/.test(p)) {
|
|
659
|
+
throw new DOMException('The subprotocol \'' + p + '\' is invalid.', 'SyntaxError');
|
|
660
|
+
}
|
|
661
|
+
var k = p.toLowerCase();
|
|
662
|
+
if (seen[k]) throw new DOMException('The subprotocol \'' + p + '\' is duplicated.', 'SyntaxError');
|
|
663
|
+
seen[k] = true;
|
|
664
|
+
}
|
|
665
|
+
return protocols;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Messages leave in the order they were sent. A Blob has to be read first,
|
|
669
|
+
// asynchronously, and was sent whenever that finished: send(blob) and then
|
|
670
|
+
// send('x') put 'x' first. Each send now joins the socket's queue; one behind
|
|
671
|
+
// a Blob still being read waits for it. bufferedAmount counts a message from
|
|
672
|
+
// send() on, as the spec does, until it is handed to the socket.
|
|
673
|
+
// ws._sendFrame(opcode, payload, cb) writes one frame.
|
|
674
|
+
function sendInOrder(ws, opcode, data, cb) {
|
|
675
|
+
var q = ws._sendQueue || (ws._sendQueue = []);
|
|
676
|
+
var item = { opcode: opcode, data: data, cb: cb || null, size: data instanceof Blob ? data.size : data.length };
|
|
677
|
+
ws._bufferedAmount += item.size;
|
|
678
|
+
q.push(item);
|
|
679
|
+
if (q.length === 1) drainSendQueue(ws);
|
|
680
|
+
}
|
|
681
|
+
function drainSendQueue(ws) {
|
|
682
|
+
var q = ws._sendQueue;
|
|
683
|
+
while (q.length > 0) {
|
|
684
|
+
var it = q[0];
|
|
685
|
+
if (it.data instanceof Blob) {
|
|
686
|
+
if (it.reading) return;
|
|
687
|
+
it.reading = true;
|
|
688
|
+
it.data.arrayBuffer().then(function (ab) { it.data = Buffer.from(ab); it.reading = false; drainSendQueue(ws); },
|
|
689
|
+
function () { q.shift(); ws._bufferedAmount -= it.size; drainSendQueue(ws); });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
q.shift();
|
|
693
|
+
ws._bufferedAmount -= it.size; // _sendFrame counts it again until written
|
|
694
|
+
ws._sendFrame(it.opcode, it.data, it.cb);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// The close frame, when messages sent before it are still queued (a Blob
|
|
699
|
+
// being read): it waits its turn, so it is the last frame out. With `now` (the
|
|
700
|
+
// peer closed first) the queue is dropped instead, and the caller writes it.
|
|
701
|
+
// true: queued, the caller writes nothing.
|
|
702
|
+
function queueCloseFrame(ws, payload, now) {
|
|
703
|
+
var q = ws._sendQueue;
|
|
704
|
+
if (!q || q.length === 0) return false;
|
|
705
|
+
if (now) {
|
|
706
|
+
for (var i = 0; i < q.length; i++) { ws._bufferedAmount -= q[i].size; if (q[i].cb) q[i].cb(new Error('the WebSocket is closed')); }
|
|
707
|
+
q.length = 0;
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
q.push({ opcode: OPCODE.CLOSE, data: payload, cb: null, size: 0 });
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Ports the browser will not connect to (Fetch, "bad port"): services where a
|
|
715
|
+
// request someone else's page made could be taken for that protocol's commands
|
|
716
|
+
// — SMTP, IRC, SSH. Node's fetch refuses them as well.
|
|
717
|
+
var BAD_PORTS = new Set([0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95,
|
|
718
|
+
101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465, 512,
|
|
719
|
+
513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720,
|
|
720
|
+
1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080]);
|
|
721
|
+
function isBadPort(port) { return BAD_PORTS.has(Number(port)); }
|
|
722
|
+
|
|
609
723
|
export {
|
|
724
|
+
userCloseCode,
|
|
725
|
+
subprotocolList,
|
|
726
|
+
closeReasonBytes,
|
|
727
|
+
sendInOrder,
|
|
728
|
+
queueCloseFrame,
|
|
729
|
+
isBadPort,
|
|
610
730
|
WSParser,
|
|
611
731
|
encodeFrame,
|
|
612
732
|
applyMask,
|
package/src/ws_client.js
CHANGED
|
@@ -17,8 +17,9 @@ import lemonTls from 'lemon-tls';
|
|
|
17
17
|
import { globalAgent } from './agent.js';
|
|
18
18
|
import {
|
|
19
19
|
WSParser, encodeFrame, generateKey, acceptKey,
|
|
20
|
-
|
|
21
|
-
installEventSurfaces, emitBoth, nodeMessagePayload
|
|
20
|
+
OPCODE, CLOSE,
|
|
21
|
+
installEventSurfaces, emitBoth, nodeMessagePayload,
|
|
22
|
+
userCloseCode, closeReasonBytes, subprotocolList, sendInOrder, queueCloseFrame, isBadPort
|
|
22
23
|
} from './ws.js';
|
|
23
24
|
|
|
24
25
|
|
|
@@ -78,6 +79,13 @@ class WebSocket extends EventTarget {
|
|
|
78
79
|
|
|
79
80
|
constructor(url, protocols, options) {
|
|
80
81
|
super();
|
|
82
|
+
if (arguments.length === 0) throw new TypeError('Failed to construct \'WebSocket\': 1 argument required, but only 0 present.');
|
|
83
|
+
// The second argument may be an options bag, { protocols } (WPT
|
|
84
|
+
// websockets/constructor/option-bag; WebSocketStream takes the same).
|
|
85
|
+
// The third is QUICO's: what a browser cannot express (headers, ca).
|
|
86
|
+
if (protocols !== null && typeof protocols === 'object' && typeof protocols[Symbol.iterator] !== 'function') {
|
|
87
|
+
protocols = protocols.protocols;
|
|
88
|
+
}
|
|
81
89
|
|
|
82
90
|
// ---- URL parsing, per the spec's constructor steps ----
|
|
83
91
|
var parsed;
|
|
@@ -97,36 +105,21 @@ class WebSocket extends EventTarget {
|
|
|
97
105
|
'The URL\'s scheme must be either \'ws\' or \'wss\'. \'' +
|
|
98
106
|
parsed.protocol.replace(':', '') + '\' is not allowed.', 'SyntaxError');
|
|
99
107
|
}
|
|
100
|
-
|
|
108
|
+
// A fragment, even an empty one ("…/#"), is a SyntaxError: parsed.hash
|
|
109
|
+
// is "" for both "no fragment" and "#", so it is read from the URL itself.
|
|
110
|
+
if (parsed.href.indexOf('#') >= 0) {
|
|
101
111
|
throw new DOMException('The URL contains a fragment identifier (\'' +
|
|
102
112
|
parsed.hash + '\'). Fragment identifiers are not allowed in WebSocket URLs.',
|
|
103
113
|
'SyntaxError');
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
if (protocols === undefined) protocols = [];
|
|
108
|
-
else if (typeof protocols === 'string') protocols = [protocols];
|
|
109
|
-
else protocols = Array.from(protocols);
|
|
110
|
-
|
|
111
|
-
var seen = Object.create(null);
|
|
112
|
-
for (var i = 0; i < protocols.length; i++) {
|
|
113
|
-
var p = String(protocols[i]);
|
|
114
|
-
// Must be a valid HTTP token and must not repeat, or the
|
|
115
|
-
// Sec-WebSocket-Protocol header would be ambiguous.
|
|
116
|
-
if (p.length === 0 || /[^\x21\x23-\x27\x2a\x2b\x2d\x2e0-9\x41-\x5a\x5e-\x7a\x7c\x7e]/.test(p)) {
|
|
117
|
-
throw new DOMException('The subprotocol \'' + p + '\' is invalid.', 'SyntaxError');
|
|
118
|
-
}
|
|
119
|
-
if (seen[p]) {
|
|
120
|
-
throw new DOMException('The subprotocol \'' + p + '\' is duplicated.', 'SyntaxError');
|
|
121
|
-
}
|
|
122
|
-
seen[p] = true;
|
|
123
|
-
protocols[i] = p;
|
|
124
|
-
}
|
|
116
|
+
protocols = subprotocolList(protocols);
|
|
125
117
|
|
|
126
118
|
options = options || {};
|
|
127
119
|
|
|
128
120
|
// ---- Public state (spec attributes) ----
|
|
129
121
|
this._url = parsed.href;
|
|
122
|
+
this._origin = parsed.origin; // a message event's origin (the URL's, not the URL)
|
|
130
123
|
this._readyState = CONNECTING;
|
|
131
124
|
this._bufferedAmount = 0;
|
|
132
125
|
this._binaryType = 'blob'; // the browser's default
|
|
@@ -233,48 +226,22 @@ class WebSocket extends EventTarget {
|
|
|
233
226
|
// 'open' is a programming error, not a race the runtime can absorb.
|
|
234
227
|
throw new DOMException('Still in CONNECTING state.', 'InvalidStateError');
|
|
235
228
|
}
|
|
236
|
-
if (this._readyState !== OPEN) {
|
|
237
|
-
// After close() the spec says to discard silently — a message that
|
|
238
|
-
// arrives during teardown is not the caller's fault.
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
var self = this;
|
|
243
229
|
var forced = options && typeof options.binary === 'boolean' ? options.binary : null;
|
|
230
|
+
var opcode, payload;
|
|
231
|
+
if (typeof data === 'string') { opcode = forced === true ? OPCODE.BINARY : OPCODE.TEXT; payload = Buffer.from(data, 'utf8'); }
|
|
232
|
+
else if (data instanceof Blob) { opcode = forced === false ? OPCODE.TEXT : OPCODE.BINARY; payload = data; }
|
|
233
|
+
else if (ArrayBuffer.isView(data)) { opcode = forced === false ? OPCODE.TEXT : OPCODE.BINARY; payload = Buffer.from(data.buffer, data.byteOffset, data.byteLength); }
|
|
234
|
+
else if (data instanceof ArrayBuffer) { opcode = forced === false ? OPCODE.TEXT : OPCODE.BINARY; payload = Buffer.from(data); }
|
|
235
|
+
else { opcode = OPCODE.TEXT; payload = Buffer.from(String(data), 'utf8'); } // stringified, as the browser does
|
|
244
236
|
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (data instanceof Blob) {
|
|
251
|
-
// Asynchronous, but bufferedAmount must reflect the bytes immediately —
|
|
252
|
-
// the spec increments it at send() time, and code that polls it to
|
|
253
|
-
// throttle would otherwise never see the backlog.
|
|
254
|
-
this._bufferedAmount += data.size;
|
|
255
|
-
var pending = data.size;
|
|
256
|
-
data.arrayBuffer().then(function (ab) {
|
|
257
|
-
self._bufferedAmount -= pending;
|
|
258
|
-
if (self._readyState === OPEN) {
|
|
259
|
-
self._sendFrame(forced === false ? OPCODE.TEXT : OPCODE.BINARY, Buffer.from(ab));
|
|
260
|
-
}
|
|
261
|
-
}, function () {
|
|
262
|
-
self._bufferedAmount -= pending;
|
|
263
|
-
});
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (ArrayBuffer.isView(data)) {
|
|
267
|
-
this._sendFrame(forced === false ? OPCODE.TEXT : OPCODE.BINARY,
|
|
268
|
-
Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (data instanceof ArrayBuffer) {
|
|
272
|
-
this._sendFrame(forced === false ? OPCODE.TEXT : OPCODE.BINARY, Buffer.from(data));
|
|
237
|
+
if (this._readyState !== OPEN) {
|
|
238
|
+
// After close() the message is dropped, but bufferedAmount grows by it
|
|
239
|
+
// all the same — the spec's way of telling a sender that nothing goes.
|
|
240
|
+
this._bufferedAmount += payload instanceof Blob ? payload.size : payload.length;
|
|
273
241
|
return;
|
|
274
242
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this._sendFrame(OPCODE.TEXT, Buffer.from(String(data), 'utf8'));
|
|
243
|
+
// In the order sent, a Blob included (ws.js sendInOrder).
|
|
244
|
+
sendInOrder(this, opcode, payload);
|
|
278
245
|
}
|
|
279
246
|
|
|
280
247
|
/** Not in the browser: keepalive for a long-lived server-side connection. */
|
|
@@ -284,21 +251,25 @@ class WebSocket extends EventTarget {
|
|
|
284
251
|
: Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
285
252
|
}
|
|
286
253
|
|
|
287
|
-
|
|
288
|
-
|
|
254
|
+
// cb(err): once the frame is handed to the socket (WebSocketStream's
|
|
255
|
+
// write() resolves there: its backpressure), or err if it cannot be.
|
|
256
|
+
_sendFrame(opcode, payload, cb) {
|
|
257
|
+
if (!this._socket || this._socket.destroyed) { if (cb) cb(new Error('the WebSocket is closed')); return; }
|
|
289
258
|
var frame;
|
|
290
259
|
try {
|
|
291
260
|
// mask: true — every client-to-server frame is masked (RFC 6455 §5.1).
|
|
292
261
|
frame = encodeFrame(opcode, payload, { mask: true });
|
|
293
262
|
} catch (e) {
|
|
263
|
+
if (cb) cb(e);
|
|
294
264
|
this._fail(e);
|
|
295
265
|
return;
|
|
296
266
|
}
|
|
297
267
|
|
|
298
268
|
var self = this;
|
|
299
269
|
if (opcode < 0x8) this._bufferedAmount += payload.length;
|
|
300
|
-
this._socket.write(frame, function () {
|
|
270
|
+
this._socket.write(frame, function (err) {
|
|
301
271
|
if (opcode < 0x8) self._bufferedAmount -= payload.length;
|
|
272
|
+
if (cb) cb(err || null);
|
|
302
273
|
});
|
|
303
274
|
}
|
|
304
275
|
|
|
@@ -308,32 +279,25 @@ class WebSocket extends EventTarget {
|
|
|
308
279
|
// ============================================================
|
|
309
280
|
|
|
310
281
|
close(code, reason) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
'InvalidAccessError');
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
var reasonBuf = Buffer.alloc(0);
|
|
320
|
-
if (reason !== undefined && reason !== null) {
|
|
321
|
-
reasonBuf = Buffer.from(String(reason), 'utf8');
|
|
322
|
-
// 125-byte control payload minus the 2-byte code.
|
|
323
|
-
if (reasonBuf.length > 123) {
|
|
324
|
-
throw new DOMException('The close reason must not be greater than 123 UTF-8 bytes.',
|
|
325
|
-
'SyntaxError');
|
|
326
|
-
}
|
|
327
|
-
}
|
|
282
|
+
// As WebIDL takes them: a code given (null too) is clamped and checked,
|
|
283
|
+
// a reason is at most 123 bytes of UTF-8 (ws.js).
|
|
284
|
+
if (code !== undefined) code = userCloseCode(code);
|
|
285
|
+
var reasonBuf = reason !== undefined ? closeReasonBytes(reason) : Buffer.alloc(0);
|
|
328
286
|
|
|
329
287
|
if (this._readyState === CLOSING || this._readyState === CLOSED) return;
|
|
330
288
|
|
|
331
289
|
if (this._readyState === CONNECTING) {
|
|
332
|
-
//
|
|
290
|
+
// The spec: "fail the WebSocket connection". Nothing was established,
|
|
291
|
+
// so there is no closing handshake: the attempt is abandoned, and —
|
|
292
|
+
// in a task of its own, after close() has returned — 'error', then
|
|
293
|
+
// 'close' with 1006, not clean. They used to fire inside close(), so a
|
|
294
|
+
// listener added on the next line missed them, and 'error' never came.
|
|
333
295
|
this._readyState = CLOSING;
|
|
334
|
-
this._closeCode =
|
|
335
|
-
this._closeReason =
|
|
336
|
-
this.
|
|
296
|
+
this._closeCode = CLOSE.ABNORMAL;
|
|
297
|
+
this._closeReason = '';
|
|
298
|
+
if (this._req) { try { this._req.destroy(); } catch (e) {} }
|
|
299
|
+
var failing = this;
|
|
300
|
+
setImmediate(function () { failing._fail(new Error('WebSocket closed before the connection was established')); });
|
|
337
301
|
return;
|
|
338
302
|
}
|
|
339
303
|
|
|
@@ -349,7 +313,9 @@ class WebSocket extends EventTarget {
|
|
|
349
313
|
if (this._closeTimer.unref) this._closeTimer.unref();
|
|
350
314
|
}
|
|
351
315
|
|
|
352
|
-
|
|
316
|
+
// `now`: answering the peer's close (what is still queued is dropped).
|
|
317
|
+
// Otherwise behind the messages sent before close() — see ws_server.js.
|
|
318
|
+
_sendCloseFrame(code, reasonBuf, now) {
|
|
353
319
|
if (this._closeSent) return;
|
|
354
320
|
this._closeSent = true;
|
|
355
321
|
|
|
@@ -361,6 +327,7 @@ class WebSocket extends EventTarget {
|
|
|
361
327
|
payload.writeUInt16BE(code, 0);
|
|
362
328
|
if (reasonBuf && reasonBuf.length) reasonBuf.copy(payload, 2);
|
|
363
329
|
}
|
|
330
|
+
if (queueCloseFrame(this, payload, now)) return;
|
|
364
331
|
|
|
365
332
|
if (this._socket && !this._socket.destroyed) {
|
|
366
333
|
try { this._socket.write(encodeFrame(OPCODE.CLOSE, payload, { mask: true })); } catch (e) {}
|
|
@@ -378,6 +345,9 @@ class WebSocket extends EventTarget {
|
|
|
378
345
|
var self = this;
|
|
379
346
|
var secure = parsed.protocol === 'wss:';
|
|
380
347
|
var port = parsed.port ? parseInt(parsed.port, 10) : (secure ? 443 : 80);
|
|
348
|
+
// A port the browser will not connect to (ws.js isBadPort): the
|
|
349
|
+
// connection fails — 'error', then 'close' — without being attempted.
|
|
350
|
+
if (isBadPort(port)) { this._fail(new Error('WebSocket connection to port ' + port + ' is not allowed (a blocked port)')); return; }
|
|
381
351
|
var key = generateKey();
|
|
382
352
|
|
|
383
353
|
var headers = {
|
|
@@ -392,8 +362,18 @@ class WebSocket extends EventTarget {
|
|
|
392
362
|
// The browser sends Origin automatically; here it is the caller's choice,
|
|
393
363
|
// because a server-side client has no origin to speak of.
|
|
394
364
|
if (this._options.origin) headers['Origin'] = this._options.origin;
|
|
365
|
+
// A user name and password in the URL (wss://user:pass@host/) go as
|
|
366
|
+
// HTTP Basic authentication, as the browser sends them; they were
|
|
367
|
+
// dropped. An Authorization the caller set wins.
|
|
368
|
+
if (parsed.username || parsed.password) {
|
|
369
|
+
headers['Authorization'] = 'Basic ' + Buffer.from(decodeURIComponent(parsed.username) + ':' +
|
|
370
|
+
decodeURIComponent(parsed.password), 'utf8').toString('base64');
|
|
371
|
+
}
|
|
395
372
|
if (this._options.headers) {
|
|
396
|
-
for (var h in this._options.headers)
|
|
373
|
+
for (var h in this._options.headers) {
|
|
374
|
+
if (h.toLowerCase() === 'authorization') delete headers['Authorization'];
|
|
375
|
+
headers[h] = this._options.headers[h];
|
|
376
|
+
}
|
|
397
377
|
}
|
|
398
378
|
|
|
399
379
|
// TLS session resumption, as quico.request has it: a session from the
|
|
@@ -518,7 +498,7 @@ class WebSocket extends EventTarget {
|
|
|
518
498
|
emitBoth(self, 'message',
|
|
519
499
|
new MessageEvent('message', {
|
|
520
500
|
data: isBinary ? self._toBinary(data) : data,
|
|
521
|
-
origin: self.
|
|
501
|
+
origin: self._origin
|
|
522
502
|
}),
|
|
523
503
|
// Raw payload for the ws surface: `ws` has no binaryType and always
|
|
524
504
|
// hands its listeners a Buffer.
|
|
@@ -550,7 +530,7 @@ class WebSocket extends EventTarget {
|
|
|
550
530
|
// the peer reports what comes back in its own close event.
|
|
551
531
|
self._readyState = CLOSING;
|
|
552
532
|
self._sendCloseFrame(code === CLOSE.NO_STATUS ? undefined : code,
|
|
553
|
-
Buffer.from(reason || '', 'utf8'));
|
|
533
|
+
Buffer.from(reason || '', 'utf8'), true);
|
|
554
534
|
}
|
|
555
535
|
self._teardown(true);
|
|
556
536
|
});
|
package/src/ws_server.js
CHANGED
|
@@ -13,8 +13,9 @@ import { Blob } from 'node:buffer';
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
WSParser, encodeFrame, acceptKey,
|
|
16
|
-
|
|
17
|
-
installEventSurfaces, emitBoth, nodeMessagePayload
|
|
16
|
+
OPCODE, CLOSE,
|
|
17
|
+
installEventSurfaces, emitBoth, nodeMessagePayload,
|
|
18
|
+
userCloseCode, closeReasonBytes, sendInOrder, queueCloseFrame
|
|
18
19
|
} from './ws.js';
|
|
19
20
|
import { isToken, STATUS_TEXT } from './http_common.js';
|
|
20
21
|
|
|
@@ -79,6 +80,7 @@ class ServerWebSocket extends EventTarget {
|
|
|
79
80
|
this._protocol = options.protocol || '';
|
|
80
81
|
this._extensions = '';
|
|
81
82
|
this._url = options.url || '';
|
|
83
|
+
try { this._origin = new URL(this._url).origin; } catch (e) { this._origin = ''; }
|
|
82
84
|
|
|
83
85
|
this._closeSent = false;
|
|
84
86
|
this._closeReceived = false;
|
|
@@ -105,7 +107,8 @@ class ServerWebSocket extends EventTarget {
|
|
|
105
107
|
emitBoth(self, 'message',
|
|
106
108
|
new MessageEvent('message', {
|
|
107
109
|
data: isBinary ? self._toBinary(data) : data,
|
|
108
|
-
origin
|
|
110
|
+
// The URL's origin, as the client's (not the whole URL).
|
|
111
|
+
origin: self._origin
|
|
109
112
|
}),
|
|
110
113
|
// The ws surface gets the raw payload, NOT self._toBinary(data): `ws`
|
|
111
114
|
// has no binaryType and its handlers always receive a Buffer.
|
|
@@ -141,7 +144,7 @@ class ServerWebSocket extends EventTarget {
|
|
|
141
144
|
// with an empty reason silently erases the reason the peer just gave).
|
|
142
145
|
self._readyState = CLOSING;
|
|
143
146
|
self._sendCloseFrame(code === CLOSE.NO_STATUS ? undefined : code,
|
|
144
|
-
Buffer.from(reason || '', 'utf8'));
|
|
147
|
+
Buffer.from(reason || '', 'utf8'), true);
|
|
145
148
|
}
|
|
146
149
|
self._teardown(true);
|
|
147
150
|
});
|
|
@@ -237,37 +240,20 @@ class ServerWebSocket extends EventTarget {
|
|
|
237
240
|
if (this._readyState === CONNECTING) {
|
|
238
241
|
throw new DOMException('Still in CONNECTING state.', 'InvalidStateError');
|
|
239
242
|
}
|
|
240
|
-
if (this._readyState !== OPEN) return;
|
|
241
|
-
|
|
242
|
-
var self = this;
|
|
243
243
|
var forced = options && typeof options.binary === 'boolean' ? options.binary : null;
|
|
244
|
-
|
|
245
|
-
if (typeof data === 'string') {
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
var opcode, payload;
|
|
245
|
+
if (typeof data === 'string') { opcode = forced === true ? OPCODE.BINARY : OPCODE.TEXT; payload = Buffer.from(data, 'utf8'); }
|
|
246
|
+
else if (data instanceof Blob) { opcode = forced === false ? OPCODE.TEXT : OPCODE.BINARY; payload = data; }
|
|
247
|
+
else if (ArrayBuffer.isView(data)) { opcode = forced === false ? OPCODE.TEXT : OPCODE.BINARY; payload = Buffer.from(data.buffer, data.byteOffset, data.byteLength); }
|
|
248
|
+
else if (data instanceof ArrayBuffer) { opcode = forced === false ? OPCODE.TEXT : OPCODE.BINARY; payload = Buffer.from(data); }
|
|
249
|
+
else { opcode = OPCODE.TEXT; payload = Buffer.from(String(data), 'utf8'); }
|
|
250
|
+
if (this._readyState !== OPEN) {
|
|
251
|
+
this._bufferedAmount += payload instanceof Blob ? payload.size : payload.length; // as the client's
|
|
248
252
|
return;
|
|
249
253
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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'));
|
|
254
|
+
// In the order sent, a Blob included (ws.js sendInOrder): a Blob was
|
|
255
|
+
// sent whenever it had been read, after messages sent later.
|
|
256
|
+
sendInOrder(this, opcode, payload);
|
|
271
257
|
}
|
|
272
258
|
|
|
273
259
|
/**
|
|
@@ -286,43 +272,33 @@ class ServerWebSocket extends EventTarget {
|
|
|
286
272
|
: Buffer.isBuffer(data) ? data : Buffer.from(data));
|
|
287
273
|
}
|
|
288
274
|
|
|
289
|
-
_sendFrame(opcode, payload) {
|
|
290
|
-
if (!this._socket || this._socket.destroyed) return;
|
|
275
|
+
_sendFrame(opcode, payload, cb) {
|
|
276
|
+
if (!this._socket || this._socket.destroyed) { if (cb) cb(new Error('the WebSocket is closed')); return; }
|
|
291
277
|
var frame;
|
|
292
278
|
// mask: false — a SERVER must never mask (RFC 6455 §5.1). This is the one
|
|
293
279
|
// line that differs from the client, and a client will close the
|
|
294
280
|
// connection with 1002 if it is wrong.
|
|
295
281
|
try { frame = encodeFrame(opcode, payload, { mask: false }); }
|
|
296
|
-
catch (e) { this._fail(e); return; }
|
|
282
|
+
catch (e) { if (cb) cb(e); this._fail(e); return; }
|
|
297
283
|
|
|
298
284
|
var self = this;
|
|
299
285
|
if (opcode < 0x8) this._bufferedAmount += payload.length;
|
|
300
|
-
this._socket.write(frame, function () {
|
|
286
|
+
this._socket.write(frame, function (err) {
|
|
301
287
|
if (opcode < 0x8) self._bufferedAmount -= payload.length;
|
|
288
|
+
if (cb) cb(err || null);
|
|
302
289
|
});
|
|
303
290
|
}
|
|
304
291
|
|
|
305
292
|
close(code, reason) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
}
|
|
293
|
+
// As the client's (ws.js): a code given, null too, is clamped and checked.
|
|
294
|
+
if (code !== undefined) code = userCloseCode(code);
|
|
295
|
+
var reasonBuf = reason !== undefined ? closeReasonBytes(reason) : Buffer.alloc(0);
|
|
321
296
|
if (this._readyState === CLOSING || this._readyState === CLOSED) return;
|
|
322
297
|
|
|
323
298
|
this._readyState = CLOSING;
|
|
324
299
|
this._closeCode = code === undefined ? CLOSE.NORMAL : code;
|
|
325
|
-
|
|
300
|
+
// As it goes on the wire: a lone surrogate is U+FFFD there (USVString).
|
|
301
|
+
this._closeReason = reasonBuf.toString('utf8');
|
|
326
302
|
this._sendCloseFrame(code === undefined ? undefined : code, reasonBuf);
|
|
327
303
|
|
|
328
304
|
var self = this;
|
|
@@ -330,7 +306,12 @@ class ServerWebSocket extends EventTarget {
|
|
|
330
306
|
if (this._closeTimer.unref) this._closeTimer.unref();
|
|
331
307
|
}
|
|
332
308
|
|
|
333
|
-
|
|
309
|
+
// `now`: answering the peer's close — what is still queued (a Blob being
|
|
310
|
+
// read) is dropped, the connection is over. Otherwise the close frame goes
|
|
311
|
+
// after the messages sent before close(), never ahead of them: a Blob still
|
|
312
|
+
// being read went out after the close frame, which a peer takes as a
|
|
313
|
+
// protocol error.
|
|
314
|
+
_sendCloseFrame(code, reasonBuf, now) {
|
|
334
315
|
if (this._closeSent) return;
|
|
335
316
|
this._closeSent = true;
|
|
336
317
|
var payload;
|
|
@@ -340,6 +321,7 @@ class ServerWebSocket extends EventTarget {
|
|
|
340
321
|
payload.writeUInt16BE(code, 0);
|
|
341
322
|
if (reasonBuf && reasonBuf.length) reasonBuf.copy(payload, 2);
|
|
342
323
|
}
|
|
324
|
+
if (queueCloseFrame(this, payload, now)) return;
|
|
343
325
|
if (this._socket && !this._socket.destroyed) {
|
|
344
326
|
try { this._socket.write(encodeFrame(OPCODE.CLOSE, payload, { mask: false })); } catch (e) {}
|
|
345
327
|
}
|