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