quico 0.4.0 → 0.8.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.
@@ -0,0 +1,621 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
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 net from 'node:net';
13
+ import { Blob } from 'node:buffer';
14
+
15
+ import { request as h1Request } from './h1_client.js';
16
+ import lemonTls from 'lemon-tls';
17
+ import { globalAgent } from './agent.js';
18
+ import {
19
+ WSParser, encodeFrame, generateKey, acceptKey,
20
+ OPCODE, CLOSE,
21
+ installEventSurfaces, emitBoth, nodeMessagePayload,
22
+ userCloseCode, closeReasonBytes, subprotocolList, sendInOrder, queueCloseFrame, isBadPort
23
+ } from './ws.js';
24
+
25
+
26
+ // ============================================================
27
+ // WebSocket — the WHATWG interface, unchanged.
28
+ //
29
+ // https://websockets.spec.whatwg.org/#the-websocket-interface
30
+ //
31
+ // Code written against a browser runs here as-is: same constructor, same
32
+ // readyState constants, same events, same close semantics, same errors. That
33
+ // is the whole design goal, and where Node lacks something the browser has
34
+ // (Blob, CloseEvent) we supply it rather than inventing a different shape.
35
+ //
36
+ // Two deliberate ADDITIONS, both invisible to browser code:
37
+ // - a third constructor argument for options a browser cannot express
38
+ // (custom headers, a CA for a private certificate). The browser ignores
39
+ // extra arguments, so passing two stays portable.
40
+ // - ping() and an onpong handler. A browser never needs these because it
41
+ // answers pings itself and its connections are supervised; a long-lived
42
+ // server-side client has no such supervision, and without a keepalive a
43
+ // dead TCP connection looks healthy for many minutes.
44
+ // ============================================================
45
+
46
+
47
+ // Node has no CloseEvent (it has Event and MessageEvent). Define the missing
48
+ // piece rather than emitting a plain object: browser code reads
49
+ // `event.wasClean` and instanceof checks should behave.
50
+ var CloseEventCtor = typeof CloseEvent !== 'undefined' ? CloseEvent : (function () {
51
+ function CloseEventPolyfill(type, init) {
52
+ init = init || {};
53
+ var ev = new Event(type, init);
54
+ Object.defineProperties(ev, {
55
+ code: { value: init.code === undefined ? 0 : init.code, enumerable: true },
56
+ reason: { value: init.reason === undefined ? '' : init.reason, enumerable: true },
57
+ wasClean: { value: !!init.wasClean, enumerable: true }
58
+ });
59
+ return ev;
60
+ }
61
+ return CloseEventPolyfill;
62
+ })();
63
+
64
+
65
+ var CONNECTING = 0, OPEN = 1, CLOSING = 2, CLOSED = 3;
66
+
67
+ // The close handshake is two-sided: after sending our close frame we wait for
68
+ // the peer's before dropping TCP (RFC 6455 §7.1.4). A peer that never answers
69
+ // must not hold the socket open forever.
70
+ var CLOSE_TIMEOUT = 30000;
71
+
72
+
73
+ class WebSocket extends EventTarget {
74
+
75
+ static get CONNECTING() { return CONNECTING; }
76
+ static get OPEN() { return OPEN; }
77
+ static get CLOSING() { return CLOSING; }
78
+ static get CLOSED() { return CLOSED; }
79
+
80
+ constructor(url, protocols, options) {
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
+ }
89
+
90
+ // ---- URL parsing, per the spec's constructor steps ----
91
+ var parsed;
92
+ try {
93
+ parsed = new URL(String(url));
94
+ } catch (e) {
95
+ throw new DOMException('The URL \'' + url + '\' is invalid.', 'SyntaxError');
96
+ }
97
+
98
+ // http/https are accepted and upgraded, which the spec added so that a
99
+ // relative or copied URL does not have to be rewritten by hand.
100
+ if (parsed.protocol === 'http:') parsed.protocol = 'ws:';
101
+ if (parsed.protocol === 'https:') parsed.protocol = 'wss:';
102
+
103
+ if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
104
+ throw new DOMException(
105
+ 'The URL\'s scheme must be either \'ws\' or \'wss\'. \'' +
106
+ parsed.protocol.replace(':', '') + '\' is not allowed.', 'SyntaxError');
107
+ }
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) {
111
+ throw new DOMException('The URL contains a fragment identifier (\'' +
112
+ parsed.hash + '\'). Fragment identifiers are not allowed in WebSocket URLs.',
113
+ 'SyntaxError');
114
+ }
115
+
116
+ protocols = subprotocolList(protocols);
117
+
118
+ options = options || {};
119
+
120
+ // ---- Public state (spec attributes) ----
121
+ this._url = parsed.href;
122
+ this._origin = parsed.origin; // a message event's origin (the URL's, not the URL)
123
+ this._readyState = CONNECTING;
124
+ this._bufferedAmount = 0;
125
+ this._binaryType = 'blob'; // the browser's default
126
+ this._protocol = '';
127
+ this._extensions = '';
128
+
129
+ // ---- Internals ----
130
+ this._socket = null;
131
+ this._parser = null;
132
+ this._req = null;
133
+ this._closeSent = false;
134
+ this._closeReceived = false;
135
+ this._closeCode = CLOSE.ABNORMAL;
136
+ this._closeReason = '';
137
+ this._closeTimer = null;
138
+ this._options = options;
139
+ this._protocols = protocols;
140
+ this._pendingPongs = [];
141
+
142
+ this._onopen = null;
143
+ this._onmessage = null;
144
+ this._onerror = null;
145
+ this._onclose = null;
146
+ this._onpong = null;
147
+
148
+ // Adds .on/.once/.off alongside the WHATWG properties. Both surfaces fire
149
+ // for every event, with the arguments each interface promises — see
150
+ // ws.js (installEventSurfaces).
151
+ installEventSurfaces(this);
152
+
153
+ // The spec's "establish a WebSocket connection" runs in parallel — the
154
+ // constructor returns before anything is on the wire, so a listener
155
+ // attached on the next line still sees 'open'.
156
+ var self = this;
157
+ queueMicrotask(function () { self._connect(parsed); });
158
+ }
159
+
160
+
161
+ // ============================================================
162
+ // Spec attributes
163
+ // ============================================================
164
+
165
+ get url() { return this._url; }
166
+ get readyState() { return this._readyState; }
167
+ get bufferedAmount() { return this._bufferedAmount; }
168
+ get protocol() { return this._protocol; }
169
+ get extensions() { return this._extensions; }
170
+
171
+ get CONNECTING() { return CONNECTING; }
172
+ get OPEN() { return OPEN; }
173
+ get CLOSING() { return CLOSING; }
174
+ get CLOSED() { return CLOSED; }
175
+
176
+ get binaryType() { return this._binaryType; }
177
+ set binaryType(value) {
178
+ // The spec permits exactly these two. 'nodebuffer' is accepted as well
179
+ // because it costs nothing and is what server-side code usually wants —
180
+ // but it is NOT the default, so browser code keeps browser behaviour.
181
+ if (value !== 'blob' && value !== 'arraybuffer' && value !== 'nodebuffer') return;
182
+ this._binaryType = value;
183
+ }
184
+
185
+ // on<event> properties. Assigning replaces the previous handler, which is
186
+ // what the browser does; addEventListener stacks, and both work together.
187
+ get onopen() { return this._onopen; }
188
+ set onopen(fn) { this._setHandler('open', '_onopen', fn); }
189
+ get onmessage() { return this._onmessage; }
190
+ set onmessage(fn) { this._setHandler('message', '_onmessage', fn); }
191
+ get onerror() { return this._onerror; }
192
+ set onerror(fn) { this._setHandler('error', '_onerror', fn); }
193
+ get onclose() { return this._onclose; }
194
+ set onclose(fn) { this._setHandler('close', '_onclose', fn); }
195
+ /** Not in the browser — see the note at the top of the file. */
196
+ get onpong() { return this._onpong; }
197
+ set onpong(fn) { this._setHandler('pong', '_onpong', fn); }
198
+
199
+ _setHandler(type, slot, fn) {
200
+ if (this[slot]) this.removeEventListener(type, this[slot]);
201
+ this[slot] = typeof fn === 'function' ? fn : null;
202
+ if (this[slot]) this.addEventListener(type, this[slot]);
203
+ }
204
+
205
+
206
+ // ============================================================
207
+ // send()
208
+ // ============================================================
209
+
210
+ /**
211
+ * send(data[, options])
212
+ *
213
+ * `options` is the ws extension, not WHATWG — a browser's send() takes one
214
+ * argument and infers text-vs-binary from the type. That inference is wrong
215
+ * for the ws surface, where every message arrives as a Buffer: echoing one
216
+ * back would turn a text message into a binary one, and the peer would
217
+ * report isBinary: true for something the sender called text.
218
+ *
219
+ * So `{ binary: false }` forces a Buffer to go out as a text frame, and
220
+ * `{ binary: true }` forces a string to go out as binary. Omitted, the
221
+ * browser's inference applies and browser code is unaffected.
222
+ */
223
+ send(data, options) {
224
+ if (this._readyState === CONNECTING) {
225
+ // The one place the spec throws rather than ignoring: sending before
226
+ // 'open' is a programming error, not a race the runtime can absorb.
227
+ throw new DOMException('Still in CONNECTING state.', 'InvalidStateError');
228
+ }
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
236
+
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;
241
+ return;
242
+ }
243
+ // In the order sent, a Blob included (ws.js sendInOrder).
244
+ sendInOrder(this, opcode, payload);
245
+ }
246
+
247
+ /** Not in the browser: keepalive for a long-lived server-side connection. */
248
+ ping(data) {
249
+ if (this._readyState !== OPEN) return;
250
+ this._sendFrame(OPCODE.PING, data == null ? Buffer.alloc(0)
251
+ : Buffer.isBuffer(data) ? data : Buffer.from(data));
252
+ }
253
+
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; }
258
+ var frame;
259
+ try {
260
+ // mask: true — every client-to-server frame is masked (RFC 6455 §5.1).
261
+ frame = encodeFrame(opcode, payload, { mask: true });
262
+ } catch (e) {
263
+ if (cb) cb(e);
264
+ this._fail(e);
265
+ return;
266
+ }
267
+
268
+ var self = this;
269
+ if (opcode < 0x8) this._bufferedAmount += payload.length;
270
+ this._socket.write(frame, function (err) {
271
+ if (opcode < 0x8) self._bufferedAmount -= payload.length;
272
+ if (cb) cb(err || null);
273
+ });
274
+ }
275
+
276
+
277
+ // ============================================================
278
+ // close()
279
+ // ============================================================
280
+
281
+ close(code, reason) {
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);
286
+
287
+ if (this._readyState === CLOSING || this._readyState === CLOSED) return;
288
+
289
+ if (this._readyState === CONNECTING) {
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.
295
+ this._readyState = CLOSING;
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')); });
301
+ return;
302
+ }
303
+
304
+ this._readyState = CLOSING;
305
+ this._sendCloseFrame(code === undefined ? undefined : code, reasonBuf);
306
+
307
+ // The peer should answer with its own close frame; when it does,
308
+ // _teardown runs with wasClean = true. This timer is the backstop.
309
+ var self = this;
310
+ this._closeTimer = setTimeout(function () {
311
+ self._teardown(false);
312
+ }, CLOSE_TIMEOUT);
313
+ if (this._closeTimer.unref) this._closeTimer.unref();
314
+ }
315
+
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) {
319
+ if (this._closeSent) return;
320
+ this._closeSent = true;
321
+
322
+ var payload;
323
+ if (code === undefined) {
324
+ payload = Buffer.alloc(0);
325
+ } else {
326
+ payload = Buffer.alloc(2 + (reasonBuf ? reasonBuf.length : 0));
327
+ payload.writeUInt16BE(code, 0);
328
+ if (reasonBuf && reasonBuf.length) reasonBuf.copy(payload, 2);
329
+ }
330
+ if (queueCloseFrame(this, payload, now)) return;
331
+
332
+ if (this._socket && !this._socket.destroyed) {
333
+ try { this._socket.write(encodeFrame(OPCODE.CLOSE, payload, { mask: true })); } catch (e) {}
334
+ }
335
+ }
336
+
337
+
338
+ // ============================================================
339
+ // Connection
340
+ // ============================================================
341
+
342
+ _connect(parsed) {
343
+ if (this._readyState !== CONNECTING) return; // closed before we started
344
+
345
+ var self = this;
346
+ var secure = parsed.protocol === 'wss:';
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; }
351
+ var key = generateKey();
352
+
353
+ var headers = {
354
+ 'Upgrade': 'websocket',
355
+ 'Connection': 'Upgrade',
356
+ 'Sec-WebSocket-Key': key,
357
+ 'Sec-WebSocket-Version': '13'
358
+ };
359
+ if (this._protocols.length) {
360
+ headers['Sec-WebSocket-Protocol'] = this._protocols.join(', ');
361
+ }
362
+ // The browser sends Origin automatically; here it is the caller's choice,
363
+ // because a server-side client has no origin to speak of.
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
+ }
372
+ if (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
+ }
377
+ }
378
+
379
+ // TLS session resumption, as quico.request has it: a session from the
380
+ // agent's cache (options.agent, or the global one) — each used once — and
381
+ // each the server sends kept there, and emitted as 'session'. An
382
+ // explicit options.session wins. wss:// began with a full handshake
383
+ // every time.
384
+ var sAgent = secure ? (this._options.agent && this._options.agent._sessionKey ? this._options.agent : globalAgent) : null;
385
+ var sKey = sAgent && this._options.agent !== false ? sAgent._sessionKey('tcp:http/1.1', parsed.hostname, port, this._options) : null;
386
+ var explicitSession = this._options.session, sessionTaken = false, sessionNow;
387
+ var reqOptions = {
388
+ hostname: parsed.hostname,
389
+ port: port,
390
+ path: (parsed.pathname || '/') + (parsed.search || ''),
391
+ method: 'GET',
392
+ headers: headers,
393
+ // wss:// means TLS whether or not the caller passed a TLS module. It
394
+ // took options.tls alone, and without one h1_client connected over
395
+ // plain TCP: to a real server the handshake hung (it waits for a
396
+ // ClientHello), and to anyone answering in the clear the upgrade —
397
+ // headers, cookies, tokens — went out unencrypted under a wss:// URL.
398
+ tls: secure ? (this._options.tls || lemonTls) : null,
399
+ ca: this._options.ca,
400
+ rejectUnauthorized: this._options.rejectUnauthorized,
401
+ // The key-exchange groups to offer (tls.connect's ecdhCurve).
402
+ ecdhCurve: this._options.ecdhCurve,
403
+ sigalgs: this._options.sigalgs,
404
+ // A WebSocket handshake must not come off a reused keep-alive socket:
405
+ // the socket is about to leave HTTP entirely, and the pool would hand it
406
+ // to another request.
407
+ pool: freshPool()
408
+ };
409
+ Object.defineProperty(reqOptions, 'session', { enumerable: true, get: function () {
410
+ if (explicitSession) return explicitSession;
411
+ if (!sessionTaken) { sessionTaken = true; sessionNow = sKey ? sAgent._takeSession(sKey) : undefined; }
412
+ return sessionNow;
413
+ } });
414
+ var req = h1Request(reqOptions);
415
+ this._req = req;
416
+ req.on('session', function (s) {
417
+ if (sKey) sAgent._cacheSession(sKey, s);
418
+ self._emitNode('session', [s]); // .on('session'), as on node:tls
419
+ });
420
+
421
+ req.on('upgrade', function (info, socket, head) {
422
+ self._onHandshake(info, socket, head, key);
423
+ });
424
+
425
+ req.on('response', function (res) {
426
+ // Any non-101 answer means the server declined the upgrade.
427
+ self._fail(new Error('Unexpected server response: ' + res.statusCode));
428
+ });
429
+
430
+ req.on('error', function (err) { self._fail(err); });
431
+
432
+ req.end();
433
+ }
434
+
435
+ _onHandshake(info, socket, head, key) {
436
+ var h = info.headers;
437
+
438
+ // §4.1 client validation. Each of these is a real check, not ceremony: a
439
+ // wrong Sec-WebSocket-Accept means whatever answered is not a WebSocket
440
+ // server, and continuing would feed frames to something that will
441
+ // interpret them as something else.
442
+ var expected = acceptKey(key);
443
+ if (String(h['sec-websocket-accept'] || '') !== expected) {
444
+ this._fail(new Error('Invalid Sec-WebSocket-Accept header'));
445
+ try { socket.destroy(); } catch (e) {}
446
+ return;
447
+ }
448
+ if (String(h['upgrade'] || '').toLowerCase() !== 'websocket') {
449
+ this._fail(new Error('Invalid Upgrade header'));
450
+ try { socket.destroy(); } catch (e) {}
451
+ return;
452
+ }
453
+
454
+ // A server may only name a subprotocol we offered (§4.1).
455
+ var chosen = h['sec-websocket-protocol'];
456
+ if (chosen !== undefined) {
457
+ chosen = String(chosen).trim();
458
+ if (this._protocols.indexOf(chosen) === -1) {
459
+ this._fail(new Error('Server sent a subprotocol that was not requested: ' + chosen));
460
+ try { socket.destroy(); } catch (e) {}
461
+ return;
462
+ }
463
+ this._protocol = chosen;
464
+ }
465
+
466
+ // We negotiate no extensions, so the server must claim none.
467
+ if (h['sec-websocket-extensions']) {
468
+ this._fail(new Error('Server sent an extension that was not requested'));
469
+ try { socket.destroy(); } catch (e) {}
470
+ return;
471
+ }
472
+
473
+ if (this._readyState !== CONNECTING) { try { socket.destroy(); } catch (e) {} return; }
474
+
475
+ this._socket = socket;
476
+ this._setupParser();
477
+
478
+ socket.setNoDelay && socket.setNoDelay(true);
479
+
480
+ this._readyState = OPEN;
481
+ emitBoth(this, 'open', new Event('open'), []);
482
+
483
+ // Frames that shared a TCP segment with the handshake response. Dropping
484
+ // them loses whatever the server sent immediately after the 101 — a bug
485
+ // that only appears when segments happen to coalesce.
486
+ if (head && head.length) this._parser.execute(head);
487
+ }
488
+
489
+ _setupParser() {
490
+ var self = this;
491
+ // isServer: false — we are the client, so incoming frames must NOT be
492
+ // masked and the parser enforces that direction.
493
+ var parser = new WSParser({ isServer: false, maxPayload: this._options.maxPayload });
494
+ this._parser = parser;
495
+
496
+ parser.on('message', function (data, isBinary) {
497
+ if (self._readyState !== OPEN) return;
498
+ emitBoth(self, 'message',
499
+ new MessageEvent('message', {
500
+ data: isBinary ? self._toBinary(data) : data,
501
+ origin: self._origin
502
+ }),
503
+ // Raw payload for the ws surface: `ws` has no binaryType and always
504
+ // hands its listeners a Buffer.
505
+ [nodeMessagePayload(data, isBinary), isBinary]);
506
+ });
507
+
508
+ parser.on('ping', function (payload) {
509
+ // Answered automatically, as a browser does. `ws` surfaces pings to its
510
+ // listeners, so the ws-side event fires; WHATWG has no 'ping' event.
511
+ if (self._readyState === OPEN) self._sendFrame(OPCODE.PONG, payload);
512
+ self._emitNode('ping', [payload]);
513
+ });
514
+
515
+ parser.on('pong', function (payload) {
516
+ emitBoth(self, 'pong', new MessageEvent('pong', { data: payload }), [payload]);
517
+ });
518
+
519
+ parser.on('close', function (code, reason) {
520
+ self._closeReceived = true;
521
+ self._closeCode = code;
522
+ // Only take the peer's reason when we did not supply one. When WE
523
+ // started the handshake the reason belongs to us, and a peer that echoes
524
+ // an empty reason must not erase what the application asked to report.
525
+ if (!self._closeSent || !self._closeReason) self._closeReason = reason;
526
+
527
+ if (!self._closeSent) {
528
+ // The peer started the handshake; echo its code AND reason back and
529
+ // finish (§7.1.4). `ws` echoes the reason too, and matching it matters:
530
+ // the peer reports what comes back in its own close event.
531
+ self._readyState = CLOSING;
532
+ self._sendCloseFrame(code === CLOSE.NO_STATUS ? undefined : code,
533
+ Buffer.from(reason || '', 'utf8'), true);
534
+ }
535
+ self._teardown(true);
536
+ });
537
+
538
+ parser.on('error', function (err) {
539
+ // A protocol violation: tell the peer why, then fail the connection.
540
+ self._sendCloseFrame(err.closeCode || CLOSE.PROTOCOL_ERROR, Buffer.alloc(0));
541
+ self._closeCode = err.closeCode || CLOSE.PROTOCOL_ERROR;
542
+ self._fail(err);
543
+ });
544
+
545
+ this._socket.on('data', function (chunk) { parser.execute(chunk); });
546
+
547
+ this._socket.on('error', function (err) { self._fail(err); });
548
+
549
+ this._socket.on('close', function () {
550
+ // TCP closed. If no close frame was exchanged this is abnormal (1006) —
551
+ // the code the spec reserves for "the connection dropped".
552
+ self._teardown(self._closeSent && self._closeReceived);
553
+ });
554
+
555
+ this._socket.on('end', function () { self._teardown(self._closeSent && self._closeReceived); });
556
+ }
557
+
558
+ _toBinary(buf) {
559
+ if (this._binaryType === 'nodebuffer') return buf;
560
+ if (this._binaryType === 'arraybuffer') {
561
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
562
+ }
563
+ return new Blob([buf]); // 'blob', the spec default
564
+ }
565
+
566
+ _fail(err) {
567
+ if (this._readyState === CLOSED) return;
568
+
569
+ // The browser's error event carries no detail, deliberately, to avoid
570
+ // leaking cross-origin information. That reason does not apply on a
571
+ // server, and a developer debugging a handshake needs to know what broke —
572
+ // so `error` is attached as an extra property. Browser code reading
573
+ // nothing from the event is unaffected.
574
+ var event = new Event('error');
575
+ try { Object.defineProperty(event, 'error', { value: err, enumerable: true }); } catch (e) {}
576
+ try { Object.defineProperty(event, 'message', { value: err && err.message, enumerable: true }); } catch (e) {}
577
+ // WHATWG gets an Event; ws gets the Error itself.
578
+ emitBoth(this, 'error', event, [err]);
579
+
580
+ this._teardown(false);
581
+ }
582
+
583
+ _teardown(wasClean) {
584
+ if (this._readyState === CLOSED) return;
585
+ this._readyState = CLOSED;
586
+
587
+ if (this._closeTimer) { clearTimeout(this._closeTimer); this._closeTimer = null; }
588
+
589
+ if (this._socket) {
590
+ var s = this._socket;
591
+ this._socket = null;
592
+ try { s.removeAllListeners('data'); } catch (e) {}
593
+ try { s.destroy(); } catch (e) {}
594
+ }
595
+
596
+ var code = wasClean ? this._closeCode : (this._closeReceived ? this._closeCode : CLOSE.ABNORMAL);
597
+ var reason = this._closeReason || '';
598
+ emitBoth(this, 'close',
599
+ new CloseEventCtor('close', { code: code, reason: reason, wasClean: !!wasClean }),
600
+ // ws passes the reason as a Buffer.
601
+ [code, Buffer.from(reason, 'utf8')]);
602
+ }
603
+ }
604
+
605
+
606
+ // A WebSocket handshake takes the socket away from HTTP for good, so it must
607
+ // never come from — or return to — the keep-alive pool.
608
+ function freshPool() {
609
+ return {
610
+ take: function () { return null; },
611
+ put: function (h, p, socket) { try { socket.destroy(); } catch (e) {} },
612
+ canOpen: function () { return true; },
613
+ acquire: function () {},
614
+ release: function () {},
615
+ destroy: function () {}
616
+ };
617
+ }
618
+
619
+
620
+ export { WebSocket };
621
+ export default WebSocket;