quico 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -33
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +9 -5
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +10 -0
- package/src/crypto.js +6 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +115 -70
- package/src/h3_client.js +25 -5
- package/src/h3_server.js +104 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +379 -63
- package/src/quic_server.js +213 -72
- package/src/quic_socket.js +5 -5
- package/src/server.js +277 -24
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +62 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +432 -57
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
package/src/ws_stream.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
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://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// WebSocketStream — the browser's streams-based WebSocket
|
|
17
|
+
//
|
|
18
|
+
// https://github.com/ricea/websocketstream-explainer
|
|
19
|
+
// (WPT websockets/stream/tentative: not yet a standard; Chrome has it)
|
|
20
|
+
//
|
|
21
|
+
// The same connection as WebSocket (ws_client.js underneath), with streams
|
|
22
|
+
// in place of events, so that both directions have backpressure:
|
|
23
|
+
//
|
|
24
|
+
// const wss = new WebSocketStream(url, { protocols, signal });
|
|
25
|
+
// const { readable, writable, protocol, extensions } = await wss.opened;
|
|
26
|
+
// for await (const message of readable) … // a string or a Uint8Array
|
|
27
|
+
// await writable.getWriter().write('hello'); // resolves once it is sent
|
|
28
|
+
// wss.close({ closeCode, reason });
|
|
29
|
+
// const { closeCode, reason } = await wss.closed;
|
|
30
|
+
//
|
|
31
|
+
// Receiving: a message nobody has read holds the socket — it is paused, TCP
|
|
32
|
+
// fills up, and the server feels it. WebSocket's 'message' events take every
|
|
33
|
+
// message as it comes, however fast, into memory.
|
|
34
|
+
// Sending: write() resolves when the frame has gone to the socket, not when
|
|
35
|
+
// it was queued — so a writer that awaits it goes as fast as the connection
|
|
36
|
+
// does. WebSocket's send() returns at once; bufferedAmount is all there is.
|
|
37
|
+
//
|
|
38
|
+
// Besides the browser's protocols and signal, the options bag takes QUICO's
|
|
39
|
+
// WebSocket options (origin, headers, ca, rejectUnauthorized, agent, …).
|
|
40
|
+
// ============================================================
|
|
41
|
+
|
|
42
|
+
import { WebSocket } from './ws_client.js';
|
|
43
|
+
import { userCloseCode, closeReasonBytes, subprotocolList, sendInOrder, OPCODE } from './ws.js';
|
|
44
|
+
|
|
45
|
+
// The browser's WebSocketError: a DOMException named 'WebSocketError', with
|
|
46
|
+
// the close code and reason. new WebSocketError(message, { closeCode, reason })
|
|
47
|
+
// takes the codes close() takes (1000, 3000–4999) and a reason of at most 123
|
|
48
|
+
// bytes; a reason without a code is 1000.
|
|
49
|
+
class WebSocketError extends DOMException {
|
|
50
|
+
constructor(message, init) {
|
|
51
|
+
super(message === undefined ? '' : String(message), 'WebSocketError');
|
|
52
|
+
if (init !== undefined && init !== null && typeof init !== 'object') throw new TypeError('WebSocketError options must be an object');
|
|
53
|
+
init = init || {};
|
|
54
|
+
var code = init.closeCode !== undefined && init.closeCode !== null ? userCloseCode(init.closeCode) : null;
|
|
55
|
+
var reason = init.reason !== undefined ? String(init.reason) : '';
|
|
56
|
+
if (reason) closeReasonBytes(reason);
|
|
57
|
+
if (code === null && reason) code = 1000;
|
|
58
|
+
Object.defineProperty(this, '_wse', { value: { closeCode: code, reason: reason } });
|
|
59
|
+
}
|
|
60
|
+
get closeCode() { return this._wse.closeCode; }
|
|
61
|
+
get reason() { return this._wse.reason; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// One of ours, with a code a constructor would refuse (1006: the connection
|
|
65
|
+
// went without a closing handshake).
|
|
66
|
+
function connectionError(message, closeCode, reason) {
|
|
67
|
+
var e = new WebSocketError(message);
|
|
68
|
+
e._wse.closeCode = closeCode;
|
|
69
|
+
e._wse.reason = reason || '';
|
|
70
|
+
return e;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function invalidState(message) { return new DOMException(message, 'InvalidStateError'); }
|
|
74
|
+
|
|
75
|
+
// A chunk written: an ArrayBuffer or a view onto one goes as a binary message
|
|
76
|
+
// (not one on shared or resizable memory: a TypeError, as the browser has
|
|
77
|
+
// it), anything else as text, stringified (a value that cannot be: TypeError).
|
|
78
|
+
function messageOf(chunk) {
|
|
79
|
+
if (chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk) ||
|
|
80
|
+
(typeof SharedArrayBuffer !== 'undefined' && chunk instanceof SharedArrayBuffer)) {
|
|
81
|
+
var buf = ArrayBuffer.isView(chunk) ? chunk.buffer : chunk;
|
|
82
|
+
if ((typeof SharedArrayBuffer !== 'undefined' && buf instanceof SharedArrayBuffer) || buf.resizable || buf.growable) {
|
|
83
|
+
throw new TypeError('a WebSocketStream cannot send shared or resizable memory');
|
|
84
|
+
}
|
|
85
|
+
var u8 = ArrayBuffer.isView(chunk) ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) : new Uint8Array(chunk);
|
|
86
|
+
return { opcode: OPCODE.BINARY, payload: Buffer.from(u8) }; // a copy: the caller may reuse its buffer
|
|
87
|
+
}
|
|
88
|
+
return { opcode: OPCODE.TEXT, payload: Buffer.from(String(chunk), 'utf8') };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class WebSocketStream {
|
|
92
|
+
constructor(url, options) {
|
|
93
|
+
if (arguments.length === 0) throw new TypeError('Failed to construct \'WebSocketStream\': 1 argument required, but only 0 present.');
|
|
94
|
+
if (options !== undefined && options !== null && typeof options !== 'object') throw new TypeError('WebSocketStream options must be an object');
|
|
95
|
+
options = options || {};
|
|
96
|
+
var protocols = subprotocolList(options.protocols, true);
|
|
97
|
+
var signal = options.signal;
|
|
98
|
+
if (signal !== undefined && signal !== null && !(signal instanceof AbortSignal)) throw new TypeError('options.signal must be an AbortSignal');
|
|
99
|
+
|
|
100
|
+
var nodeOptions = {};
|
|
101
|
+
for (var k in options) if (k !== 'protocols' && k !== 'signal') nodeOptions[k] = options[k];
|
|
102
|
+
|
|
103
|
+
// The URL is checked as WebSocket checks it (a SyntaxError); its
|
|
104
|
+
// connection starts on the next turn — not at all if aborted by then.
|
|
105
|
+
var ws = this._ws = new WebSocket(url, protocols, nodeOptions);
|
|
106
|
+
ws.binaryType = 'nodebuffer';
|
|
107
|
+
this._url = ws.url;
|
|
108
|
+
|
|
109
|
+
var self = this;
|
|
110
|
+
var openedResolve, openedReject, closedResolve, closedReject;
|
|
111
|
+
this.opened = new Promise(function (res, rej) { openedResolve = res; openedReject = rej; });
|
|
112
|
+
this.closed = new Promise(function (res, rej) { closedResolve = res; closedReject = rej; });
|
|
113
|
+
this.opened.catch(function () {});
|
|
114
|
+
this.closed.catch(function () {});
|
|
115
|
+
|
|
116
|
+
var state = 'connecting'; // connecting → open → closed
|
|
117
|
+
var pendingWrites = 0;
|
|
118
|
+
var readCtl = null, writeCtl = null, writableClosing = null;
|
|
119
|
+
this._state = function () { return state; };
|
|
120
|
+
|
|
121
|
+
function settleFailed(err) {
|
|
122
|
+
if (state === 'closed') return;
|
|
123
|
+
state = 'closed';
|
|
124
|
+
openedReject(err);
|
|
125
|
+
closedReject(err);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The signal: before the connection is made, it is never attempted;
|
|
129
|
+
// during the handshake, it is abandoned; once open, it does nothing.
|
|
130
|
+
function onAbort() {
|
|
131
|
+
if (state !== 'connecting') return;
|
|
132
|
+
var reason = signal.reason !== undefined ? signal.reason : new DOMException('The operation was aborted.', 'AbortError');
|
|
133
|
+
settleFailed(reason);
|
|
134
|
+
try { ws.close(); } catch (e) {}
|
|
135
|
+
}
|
|
136
|
+
if (signal) {
|
|
137
|
+
if (signal.aborted) { onAbort(); return; }
|
|
138
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ws.addEventListener('open', function () {
|
|
142
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
143
|
+
if (state !== 'connecting') return;
|
|
144
|
+
state = 'open';
|
|
145
|
+
var socket = ws._socket;
|
|
146
|
+
|
|
147
|
+
var readable = new ReadableStream({
|
|
148
|
+
start: function (c) { readCtl = c; },
|
|
149
|
+
// Read: the socket goes on. Unread: it is paused (see 'message').
|
|
150
|
+
pull: function () { if (socket && !socket.destroyed && socket.isPaused()) socket.resume(); },
|
|
151
|
+
cancel: function (reason) { closeWith(reason); }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
var writable = new WritableStream({
|
|
155
|
+
start: function (c) { writeCtl = c; },
|
|
156
|
+
write: function (chunk) {
|
|
157
|
+
var m = messageOf(chunk);
|
|
158
|
+
if (ws.readyState !== WebSocket.OPEN) throw invalidState('the WebSocket is closed');
|
|
159
|
+
return new Promise(function (resolve, reject) {
|
|
160
|
+
pendingWrites++;
|
|
161
|
+
sendInOrder(ws, m.opcode, m.payload, function (err) {
|
|
162
|
+
pendingWrites--;
|
|
163
|
+
if (err) reject(invalidState('the WebSocket closed before the message was sent'));
|
|
164
|
+
else resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
// A clean close, no code; done once the closing handshake is.
|
|
169
|
+
close: function () {
|
|
170
|
+
return new Promise(function (resolve) {
|
|
171
|
+
writableClosing = resolve;
|
|
172
|
+
try { ws.close(); } catch (e) { resolve(); }
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
abort: function (reason) { closeWith(reason); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
openedResolve({ readable: readable, writable: writable, protocol: ws.protocol, extensions: ws.extensions });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
ws.addEventListener('message', function (e) {
|
|
182
|
+
if (!readCtl) return;
|
|
183
|
+
var data = typeof e.data === 'string' ? e.data : new Uint8Array(e.data);
|
|
184
|
+
try { readCtl.enqueue(data); } catch (x) { return; }
|
|
185
|
+
// Nobody reading: hold the socket until somebody does. The server's
|
|
186
|
+
// sends then wait on TCP — the receiving side's backpressure.
|
|
187
|
+
var socket = ws._socket;
|
|
188
|
+
if (readCtl.desiredSize <= 0 && socket && !socket.isPaused()) socket.pause();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
ws.addEventListener('close', function (e) {
|
|
192
|
+
if (signal) signal.removeEventListener('abort', onAbort);
|
|
193
|
+
if (writableClosing) { var done = writableClosing; writableClosing = null; done(); }
|
|
194
|
+
if (state === 'connecting') {
|
|
195
|
+
settleFailed(connectionError('the WebSocket connection could not be established', 1006, ''));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (state === 'closed') return;
|
|
199
|
+
state = 'closed';
|
|
200
|
+
if (e.wasClean && pendingWrites === 0) {
|
|
201
|
+
// Clean: the peer's code and reason. Nothing more can be written.
|
|
202
|
+
closedResolve({ closeCode: e.code, reason: e.reason });
|
|
203
|
+
try { readCtl.close(); } catch (x) {}
|
|
204
|
+
try { writeCtl.error(invalidState('the WebSocket is closed')); } catch (x) {}
|
|
205
|
+
} else {
|
|
206
|
+
// Not clean — no closing handshake, or one that left messages
|
|
207
|
+
// unsent: the same error for closed, the readable and the writable.
|
|
208
|
+
var err = connectionError(e.wasClean ? 'the WebSocket closed with messages unsent' : 'the WebSocket connection was lost',
|
|
209
|
+
e.wasClean ? e.code : 1006, e.wasClean ? e.reason : '');
|
|
210
|
+
closedReject(err);
|
|
211
|
+
try { readCtl.error(err); } catch (x) {}
|
|
212
|
+
try { writeCtl.error(err); } catch (x) {}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// abort() of the writable, cancel() of the readable: closed with the
|
|
217
|
+
// code and reason of a WebSocketError given as the reason; anything else
|
|
218
|
+
// is a close with no code.
|
|
219
|
+
function closeWith(reason) {
|
|
220
|
+
if (reason instanceof WebSocketError && reason.closeCode !== null) {
|
|
221
|
+
try { ws.close(reason.closeCode, reason.reason); } catch (e) { try { ws.close(); } catch (x) {} }
|
|
222
|
+
} else {
|
|
223
|
+
try { ws.close(); } catch (e) {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
get url() { return this._url; }
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* close({ closeCode, reason }): as WebSocket's close() checks them; a
|
|
233
|
+
* reason without a code is 1000, neither is a close with no code (1005 at
|
|
234
|
+
* the peer). During the handshake: abandoned — opened and closed reject.
|
|
235
|
+
*/
|
|
236
|
+
close(closeInfo) {
|
|
237
|
+
if (closeInfo !== undefined && closeInfo !== null && typeof closeInfo !== 'object') {
|
|
238
|
+
throw new TypeError('close() takes { closeCode, reason }');
|
|
239
|
+
}
|
|
240
|
+
var code = closeInfo && closeInfo.closeCode !== undefined ? userCloseCode(closeInfo.closeCode) : undefined;
|
|
241
|
+
var reason = closeInfo && closeInfo.reason !== undefined ? String(closeInfo.reason) : '';
|
|
242
|
+
closeReasonBytes(reason);
|
|
243
|
+
if (code === undefined && reason) code = 1000;
|
|
244
|
+
if (this._state() === 'closed') return;
|
|
245
|
+
if (this._state() === 'connecting') { try { this._ws.close(); } catch (e) {} return; }
|
|
246
|
+
this._ws.close(code, code === undefined ? undefined : reason);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { WebSocketStream, WebSocketError };
|