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.
- package/README.md +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -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 +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/ws_client.js
ADDED
|
@@ -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;
|