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/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
- isValidUserCloseCode, OPCODE, CLOSE,
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
- if (parsed.hash) {
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
- // ---- Subprotocols ----
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 (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
- // 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
- // Anything else is stringified, as the browser does.
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
- _sendFrame(opcode, payload) {
288
- if (!this._socket || this._socket.destroyed) return;
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
- if (code !== undefined && code !== null) {
312
- code = Number(code);
313
- if (!isValidUserCloseCode(code)) {
314
- throw new DOMException('The close code must be 1000 or between 3000 and 4999.',
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
- // Nothing was ever established, so there is no handshake to complete.
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 = code || CLOSE.ABNORMAL;
335
- this._closeReason = reason ? String(reason) : '';
336
- this._teardown(false);
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
- _sendCloseFrame(code, reasonBuf) {
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) headers[h] = this._options.headers[h];
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._url
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
- isValidUserCloseCode, OPCODE, CLOSE,
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: self._url
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
- this._sendFrame(forced === true ? OPCODE.BINARY : OPCODE.TEXT,
247
- Buffer.from(data, 'utf8'));
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
- 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'));
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
- 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
- }
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
- this._closeReason = reason ? String(reason) : '';
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
- _sendCloseFrame(code, reasonBuf) {
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
  }