quico 0.7.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 +245 -31
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +7 -4
- package/src/connect_udp_client.js +9 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +29 -60
- package/src/h3_client.js +24 -5
- package/src/h3_server.js +101 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/quic_connection.js +200 -23
- package/src/quic_server.js +94 -13
- package/src/server.js +266 -22
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +4 -0
- package/src/webtransport.js +424 -54
- 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/README.md
CHANGED
|
@@ -288,12 +288,15 @@ QUICO takes from Node is `node:net` (TCP), `node:dgram` (UDP) and `node:crypto`
|
|
|
288
288
|
- Bidirectional and unidirectional streams (Node.js Duplex API)
|
|
289
289
|
- Unreliable datagrams
|
|
290
290
|
- Natural API: `req.on('stream')`, `res.sendDatagram()`, `res.writeHead(200)` to accept
|
|
291
|
+
- Over HTTP/3 only: a session asked for over HTTP/1.1 or HTTP/2 is answered `501`,
|
|
292
|
+
with no body, before the application sees it
|
|
291
293
|
|
|
292
294
|
### WebSocket
|
|
293
295
|
- Server over all three versions: HTTP/1.1 Upgrade, HTTP/2 extended CONNECT
|
|
294
296
|
(RFC 8441) and HTTP/3 extended CONNECT (RFC 9220) — one `'upgrade'` event for all
|
|
295
|
-
- `WebSocketServer` in the shape of the `ws` package, and
|
|
296
|
-
`WebSocket` client (HTTP/1.1)
|
|
297
|
+
- `WebSocketServer` in the shape of the `ws` package, and the browser's
|
|
298
|
+
`WebSocket` client (HTTP/1.1), checked against the WPT `websockets/` tests
|
|
299
|
+
- `WebSocketStream` (experimental): the streams version, with backpressure both ways
|
|
297
300
|
|
|
298
301
|
### CONNECT-UDP
|
|
299
302
|
- A UDP proxy (RFC 9298, MASQUE) over HTTP/3 — QUIC datagrams — and HTTP/2 and
|
|
@@ -1006,6 +1009,7 @@ Creates a server for HTTP/3, HTTP/2 and HTTP/1.1. Returns a server object with
|
|
|
1006
1009
|
| `handshakeLoadThreshold` | `number` | `500` | HTTP/3 handshakes under way at which `'handshakeLoad'` fires (`high: true`), and again under half of it (`high: false`). `0`: off. |
|
|
1007
1010
|
| `maxHandshakesPerAddress` | `number` | `64` | HTTP/3 handshakes under way from one client address; past it an Initial from there makes no connection. `0`: no cap. |
|
|
1008
1011
|
| `resetTokenSecret` | `Buffer` (≥ 16 bytes) | random | Secret for QUIC stateless-reset tokens. Keep it stable across restarts so clients of a crashed instance get a reset instead of a timeout. |
|
|
1012
|
+
| `tokenSecret` | `Buffer` (≥ 16 bytes) | random | Secret the HTTP/3 address-validation tokens (Retry, and NEW_TOKEN for a client's next connection) are sealed under — node:quic's name. Servers that share it open each other's tokens: several on one port (a thread each, `SO_REUSEPORT`), or one restarted. |
|
|
1009
1013
|
| `socket` | `dgram.Socket` | `null` | Use an externally-bound IPv4 UDP socket instead of creating one. Must already be bound. |
|
|
1010
1014
|
| `socket6` | `dgram.Socket` | `null` | Same, for IPv6. |
|
|
1011
1015
|
| `greaseQuicBit` | `boolean` | `true`; `false` with `socket` / `socket6` | QUIC: invite clients to send the fixed bit unpredictable (RFC 9287), so the bit stays free to change. Off by default on an external socket, whose port may be shared with STUN, TURN or DTLS — told apart from QUIC by that bit (RFC 9443). `true` when that port carries QUIC alone. |
|
|
@@ -1087,22 +1091,54 @@ HTTP/1.1 and h2c.
|
|
|
1087
1091
|
- `server.listen(port[, host][, callback])` — start listening. Binds UDP (unless
|
|
1088
1092
|
`http3: false` or external sockets were given) and TCP (unless both fallbacks are
|
|
1089
1093
|
disabled). Port `0` picks a free port.
|
|
1090
|
-
- `server.close([callback])` —
|
|
1094
|
+
- `server.close([callback])` — as `node:http`'s: no new connections, the idle ones
|
|
1095
|
+
closed at once, and each with a request in flight closed once it is answered — HTTP/2
|
|
1096
|
+
and HTTP/3 with `GOAWAY`, HTTP/1.1 at the end of its response. Then `'close'` and the
|
|
1097
|
+
callback. A second `close()`: the callback gets `ERR_SERVER_NOT_RUNNING`.
|
|
1098
|
+
- `server.closeIdleConnections()` / `server.closeAllConnections()` — as `node:http`'s,
|
|
1099
|
+
on all three versions: the connections with no request in flight (HTTP/2 and HTTP/3
|
|
1100
|
+
are sent `GOAWAY` first), or every one. An HTTP/1.1 socket handed to `'upgrade'` is not
|
|
1101
|
+
the server's any more and stays, as in Node; over HTTP/2 and HTTP/3 a WebSocket or
|
|
1102
|
+
WebTransport session lives on the connection, and closes with it.
|
|
1103
|
+
- `server.getConnections(cb)`, `server.listening`, `server.ref()` / `server.unref()`,
|
|
1104
|
+
`await using server = …` (`Symbol.asyncDispose`) — as `node:net`'s; the count is TCP
|
|
1105
|
+
and QUIC connections.
|
|
1106
|
+
- `server.headersTimeout`, `server.requestTimeout`, `server.maxRequestsPerSocket` (all
|
|
1107
|
+
versions), `server.keepAliveTimeout`, `server.maxHeadersCount` (HTTP/1.1) — readable
|
|
1108
|
+
and settable while the server runs, from the next request or connection on.
|
|
1109
|
+
- The server is an `EventEmitter`, as `node:http`'s: every method (`listeners`,
|
|
1110
|
+
`removeAllListeners`, `prependListener`, …), and `instanceof EventEmitter`.
|
|
1111
|
+
- `server.setSecureContext({ key, cert[, ca, passphrase] })` — as `node:tls`'s: the
|
|
1112
|
+
certificate from the next handshake on, over HTTP/1.1, HTTP/2 and HTTP/3 alike;
|
|
1113
|
+
connections already open keep theirs. How a renewed certificate (Let's Encrypt) is taken
|
|
1114
|
+
without a restart. A key that does not match its certificate, or a missing one, throws —
|
|
1115
|
+
and the certificate in use stays.
|
|
1116
|
+
- `server.addContext(hostname, context)` — as `node:tls`'s: a certificate for one name, by
|
|
1117
|
+
the client's SNI (`'*.example.com'` covers one label; the latest added wins), on all
|
|
1118
|
+
three versions. `context`: `{ key, cert }` or `quico.createSecureContext()`. With an
|
|
1119
|
+
`SNICallback` given to `createServer`, it decides instead (as in Node); when it answers
|
|
1120
|
+
with nothing, the `setSecureContext()` certificate is used.
|
|
1091
1121
|
- `server.address()`
|
|
1092
1122
|
- `server.handleConnection(socket)` — serve a TCP socket accepted elsewhere.
|
|
1093
1123
|
- `server.setTimeout([msecs][, callback])` / `server.timeout` — as in `node:http`:
|
|
1094
1124
|
a connection idle for `msecs` emits `'timeout'` with its socket, and is destroyed
|
|
1095
1125
|
if nothing listens. `0` (default) disables it; changes apply to new connections.
|
|
1096
1126
|
TCP connections (HTTP/1.1, HTTP/2) — an HTTP/3 connection has QUIC's idle timeout.
|
|
1097
|
-
- `server.handlePacket(msg, rinfo)` / `server.hasConnection(rinfo)` — feed UDP
|
|
1098
|
-
datagrams yourself when sharing a port (external socket mode).
|
|
1127
|
+
- `server.handlePacket(msg, rinfo)` / `server.hasConnection(rinfo[, msg])` — feed UDP
|
|
1128
|
+
datagrams yourself when sharing a port (external socket mode). `hasConnection(rinfo)`
|
|
1129
|
+
asks by the sender's address; `hasConnection(rinfo, msg)` by the connection ID the
|
|
1130
|
+
datagram is addressed to — for servers sharing a port across threads, where a client
|
|
1131
|
+
whose address changed can reach one that does not hold its connection. Ask it before
|
|
1132
|
+
`handlePacket`: given a short-header packet for an ID it does not know, `handlePacket`
|
|
1133
|
+
answers with a Stateless Reset, which would end a connection another server holds.
|
|
1099
1134
|
- `server.addProtocol(name, handler, { sniff? })` — serve another protocol on the same
|
|
1100
1135
|
TCP port: by ALPN `name`, or for TLS connections without ALPN, by
|
|
1101
1136
|
`sniff(firstBytes) → true | false | null`. `handler(socket, protocol)`.
|
|
1102
1137
|
- `server.getTicketKeys()` / `setTicketKeys(keys)`,
|
|
1103
1138
|
`getEchKeys()` / `setEchKeys(ech)`,
|
|
1104
1139
|
`getValidateAddress()` / `setValidateAddress(on)`,
|
|
1105
|
-
`getResetTokenSecret()` / `setResetTokenSecret(secret)
|
|
1140
|
+
`getResetTokenSecret()` / `setResetTokenSecret(secret)`,
|
|
1141
|
+
`getTokenSecret()` / `setTokenSecret(secret)` — change these at runtime.
|
|
1106
1142
|
- `server.setOptions(options)` / `server.getOptions()` — every other option that can
|
|
1107
1143
|
change while the server runs, in one call, by the names `createServer` takes. See
|
|
1108
1144
|
[Changing options while running](#changing-options-while-running).
|
|
@@ -1513,7 +1549,7 @@ Call `agent.destroy()` to close everything it pools.
|
|
|
1513
1549
|
|
|
1514
1550
|
### `quico.createSecureContext(options)`
|
|
1515
1551
|
|
|
1516
|
-
Creates a TLS context for
|
|
1552
|
+
Creates a TLS context for `SNICallback`, `server.setSecureContext()` and `server.addContext()`. Accepts `key`, `cert`, `ca` and `passphrase`.
|
|
1517
1553
|
Re-exported from LemonTLS.
|
|
1518
1554
|
|
|
1519
1555
|
---
|
|
@@ -1727,7 +1763,15 @@ quico.createServer({ key: KEY, cert: CERT }, app).listen(4433);
|
|
|
1727
1763
|
// → { "protocol": "3.0" }
|
|
1728
1764
|
```
|
|
1729
1765
|
|
|
1730
|
-
Express, Fastify
|
|
1766
|
+
Express (4 and 5), Fastify (`serverFactory`), Koa and socket.io (`new Server(server)`,
|
|
1767
|
+
polling and WebSocket) run on it unchanged, over all three versions — checked against
|
|
1768
|
+
their npm releases.
|
|
1769
|
+
|
|
1770
|
+
`req` and `res` are `node:http`'s, down to the details frameworks rely on:
|
|
1771
|
+
`res.writeHead(status, [name, value, …])` and `[[name, value], …]`, `appendHeader`,
|
|
1772
|
+
`setHeaders(Headers | Map)`, `getRawHeaderNames`, `getHeaders()` (no prototype),
|
|
1773
|
+
`sendDate`, `strictContentLength`, `writeProcessing()` (102), `end(cb)` after the
|
|
1774
|
+
`'finish'` listeners; `req.headersDistinct`, `req.trailersDistinct`, `req.upgrade`.
|
|
1731
1775
|
|
|
1732
1776
|
Multi-domain setups use `SNICallback`, exactly as in `node:https`:
|
|
1733
1777
|
|
|
@@ -1780,6 +1824,21 @@ quico.createServer({ key: KEY, cert: CERT }, (req, res) => {
|
|
|
1780
1824
|
}).listen(4433);
|
|
1781
1825
|
```
|
|
1782
1826
|
|
|
1827
|
+
**Only over HTTP/3.** A WebTransport session is an extended CONNECT over HTTP/3 (RFC 9220).
|
|
1828
|
+
When one is asked for over HTTP/1.1 or HTTP/2, the server answers it itself: **`501` with no
|
|
1829
|
+
body** (`content-length: 0`, `cache-control: no-store`). The request never reaches the handler,
|
|
1830
|
+
`'request'` or `'upgrade'`. Two signs mark such a request, and either one is enough:
|
|
1831
|
+
|
|
1832
|
+
- the browser's Fetch destination, `Sec-Fetch-Dest: webtransport`;
|
|
1833
|
+
- an extended CONNECT for `:protocol: webtransport` over HTTP/2.
|
|
1834
|
+
|
|
1835
|
+
A browser that cannot reach HTTP/3 may still send its session request over TCP. Firefox 158 does
|
|
1836
|
+
this when HTTP/3 has been excluded for an origin: it sends a plain `GET` with
|
|
1837
|
+
`Sec-Fetch-Dest: webtransport` on a new HTTP/1.1 connection. If that request were answered as a
|
|
1838
|
+
page, the body would land in the browser's WebTransport session, and Firefox would abort the whole
|
|
1839
|
+
browser. With the `501`, the session fails cleanly (`wt.ready` rejects in the page), and the page
|
|
1840
|
+
can fall back to WebSocket.
|
|
1841
|
+
|
|
1783
1842
|
**Datagrams** (`res.sendDatagram(data)`) are never retransmitted, but they are sent under
|
|
1784
1843
|
the connection's congestion control (RFC 9221 §5.4): they wait for room in a packet and
|
|
1785
1844
|
go ahead of stream data. While the path cannot keep up, the oldest are dropped — any that
|
|
@@ -1789,14 +1848,27 @@ the peer takes no datagrams). Datagrams sent right after accepting the session a
|
|
|
1789
1848
|
until the `200` has left: a browser drops datagrams for a session it does not know yet.
|
|
1790
1849
|
|
|
1791
1850
|
`res.datagrams` is the browser's datagrams object too — the client's `wt.datagrams`,
|
|
1792
|
-
the same code: `readable`, `
|
|
1793
|
-
`
|
|
1794
|
-
`
|
|
1795
|
-
|
|
1851
|
+
the same code: `readable`, `createWritable({ sendOrder, sendGroup })`, `maxDatagramSize`,
|
|
1852
|
+
`incomingMaxAge`, `outgoingMaxAge`, `incomingMaxBufferedDatagrams` and
|
|
1853
|
+
`outgoingMaxBufferedDatagrams`, as the current W3C spec has them. Each writable has its own
|
|
1854
|
+
queue and backpressure: `writer.ready` waits while `outgoingMaxBufferedDatagrams` of its
|
|
1855
|
+
datagrams wait to go. A write that is not an `ArrayBuffer` or a view onto one, or is
|
|
1856
|
+
empty, rejects with a `TypeError`; one larger than `maxDatagramSize` is dropped. The
|
|
1857
|
+
setters take values as the browser's do: an age of `0` is no limit (`null`), a negative
|
|
1858
|
+
one is a `RangeError`; a buffer count below 1 is 1. Its `outgoingMaxAge` is the session's
|
|
1859
|
+
own: sessions on one connection share its queue, each with its own age limit and its own
|
|
1860
|
+
count of datagrams dropped. The names from before the spec changed still work, for code
|
|
1861
|
+
written then: `datagrams.writable` (one writable, made on first use) and
|
|
1862
|
+
`incomingHighWaterMark` / `outgoingHighWaterMark` (the same settings as
|
|
1863
|
+
`…MaxBufferedDatagrams`).
|
|
1796
1864
|
|
|
1797
1865
|
**Statistics** — `res.getStats()`, as the client's `wt.getStats()` (the same function):
|
|
1798
|
-
a Promise of the browser's `WebTransportConnectionStats` — `bytesSent
|
|
1799
|
-
|
|
1866
|
+
a Promise of the browser's `WebTransportConnectionStats` — `bytesSent` (the application's
|
|
1867
|
+
bytes: new stream data and datagram payloads, without framing or retransmissions),
|
|
1868
|
+
`bytesSentOverhead` (what the packets carried besides), `bytesAcknowledged` (the payload the
|
|
1869
|
+
peer acknowledged), `packetsSent`, `packetsLost` and `bytesLost` (packets of stream data or
|
|
1870
|
+
datagrams declared lost, and their size), `bytesReceived`, `packetsReceived` (whole
|
|
1871
|
+
packets), `smoothedRtt`,
|
|
1800
1872
|
`rttVariation`, `minRtt` (ms), `estimatedSendRate` (bits/s, the congestion control's
|
|
1801
1873
|
bandwidth estimate; `null` until measured), `atSendCapacity` (the congestion window is
|
|
1802
1874
|
full) — with QUICO's `congestionWindow` and `bytesInFlight` besides, and `datagrams`:
|
|
@@ -1806,6 +1878,13 @@ that has to choose what to drop for a slow receiver reads `atSendCapacity`, or
|
|
|
1806
1878
|
`bytesInFlight` against `congestionWindow`: the congestion is the connection's, not one
|
|
1807
1879
|
stream's.
|
|
1808
1880
|
|
|
1881
|
+
**Keying material** — `res.exportKeyingMaterial(label, context, outputLength)`, as the
|
|
1882
|
+
client's `wt.exportKeyingMaterial()`: a Promise of `outputLength` bytes (1 to 4096) from the
|
|
1883
|
+
TLS exporter, this session's own (draft-ietf-webtrans-http3: the label
|
|
1884
|
+
`EXPORTER-WebTransport`, the session ID, the application's label and context, each at most
|
|
1885
|
+
255 bytes). Both ends get the same bytes for the same label and context, which lets a token
|
|
1886
|
+
be bound to the session it was sent on.
|
|
1887
|
+
|
|
1809
1888
|
**Priorities** are the browser's: a stream's `sendOrder` and `sendGroup`
|
|
1810
1889
|
(`stream.sendGroup = res.createSendGroup()`), and `res.sendDatagram(data, { sendOrder,
|
|
1811
1890
|
sendGroup })` as the browser's `datagrams.createWritable({ sendOrder, sendGroup })`. The
|
|
@@ -1834,7 +1913,7 @@ stream are independent:
|
|
|
1834
1913
|
| `'committed'` | Our sending part is "all data committed" (draft-ietf-webtrans-overview §4.3; "Data Recvd", RFC 9000 §3.1): every byte written and the FIN acknowledged by the peer. Once; never for a stream reset first. `'finish'` (and `end()`'s callback) only says the bytes were handed to QUIC — a delivery deadline (MOQT's SUBGROUP_DELIVERY_TIMEOUT) runs from the FIN to here, and `resetStream()` before it drops what is left. `sendState` is then `'committed'`. |
|
|
1835
1914
|
| `stream.id`, `stream.sessionId` | Its QUIC stream ID, and its session's — read-only. |
|
|
1836
1915
|
| `stream.sendState`, `stream.recvState` | Each direction, read-only: `'open'`, `'finished'` (the FIN sent / received), `'committed'` (ours: all of it and the FIN acknowledged), `'reset'` (we gave up sending / the peer did), `'stopped'` (the peer asked us to stop / we stopped reading); `null` for the direction a unidirectional stream does not have. A `'drain'` says the writer may write; `sendState` says whether what it writes still goes anywhere. |
|
|
1837
|
-
| `'close'` | Once both directions are over — by FIN, reset or stop, from either side, in any order. When the peer's reset or stop is what ends the last one, an `Error` (`ERR_WEBTRANSPORT_STREAM_RESET` / `ERR_WEBTRANSPORT_STOP_SENDING`) goes to an `'error'` listener — only if there is one. |
|
|
1916
|
+
| `'close'` | Once both directions are over — by FIN, reset or stop, from either side, in any order. When the peer's reset or stop is what ends the last one, an `Error` (`ERR_WEBTRANSPORT_STREAM_RESET` / `ERR_WEBTRANSPORT_STOP_SENDING`) goes to an `'error'` listener — only if there is one. `pipe()` adds a listener of its own that re-throws, as it does for a `net.Socket`: a stream piped with `pipe()` needs an `'error'` listener of yours, or use `stream.pipeline()`. The session's end is not such a cancel: when the peer closes the session it resets the streams with `WT_SESSION_GONE`, and they close without an `'error'`. |
|
|
1838
1917
|
|
|
1839
1918
|
**Opening streams** is the browser's: `res.createBidirectionalStream(options[, callback])`
|
|
1840
1919
|
and `res.createUnidirectionalStream(options[, callback])` take `{ sendOrder, sendGroup,
|
|
@@ -1902,19 +1981,20 @@ const uni = await wt.createUnidirectionalStream(); // a WritableStre
|
|
|
1902
1981
|
await uni.getWriter().abort(new WebTransportError({ streamErrorCode: 7 })); // RESET_STREAM, code 7
|
|
1903
1982
|
|
|
1904
1983
|
const incoming = wt.incomingUnidirectionalStreams.getReader(); // ReadableStream of ReadableStreams
|
|
1905
|
-
const dgWriter = wt.datagrams.
|
|
1984
|
+
const dgWriter = wt.datagrams.createWritable().getWriter(); // and wt.datagrams.readable
|
|
1906
1985
|
|
|
1907
1986
|
wt.close({ closeCode: 0, reason: 'done' });
|
|
1908
1987
|
const { closeCode, reason } = await wt.closed; // the server's, if it closed first
|
|
1909
1988
|
```
|
|
1910
1989
|
|
|
1911
|
-
The whole browser API is there: `ready`, `closed` (
|
|
1990
|
+
The whole browser API is there: `ready`, `closed` (see [How the session ends](#how-the-session-ends) below),
|
|
1912
1991
|
`draining`, `close()`, `createBidirectionalStream()` / `createUnidirectionalStream()` with
|
|
1913
1992
|
`sendOrder`, `sendGroup` and `waitUntilAvailable`, `incomingBidirectionalStreams`,
|
|
1914
|
-
`incomingUnidirectionalStreams`, `datagrams` (`readable`, `
|
|
1915
|
-
`maxDatagramSize`, `incomingMaxAge`, `outgoingMaxAge`, `
|
|
1916
|
-
`
|
|
1917
|
-
`congestionControl`, `protocol`,
|
|
1993
|
+
`incomingUnidirectionalStreams`, `datagrams` (`readable`, `createWritable()`,
|
|
1994
|
+
`maxDatagramSize`, `incomingMaxAge`, `outgoingMaxAge`, `incomingMaxBufferedDatagrams`,
|
|
1995
|
+
`outgoingMaxBufferedDatagrams` — see the server's `res.datagrams` above), `createSendGroup()`, `getStats()`, `reliability`,
|
|
1996
|
+
`congestionControl`, `protocol`, `responseHeaders`, `exportKeyingMaterial()`,
|
|
1997
|
+
`WebTransport.supportsReliableOnly` (`false`: HTTP/3 only), and stream `getStats()`; the options
|
|
1918
1998
|
`serverCertificateHashes` (the leaf's SHA-256, a certificate valid for at most 14 days, as
|
|
1919
1999
|
the browser requires), `protocols`, `requireUnreliable`, `congestionControl`,
|
|
1920
2000
|
`transportParams` (QUICO's own: the windows and stream limits, as the server's),
|
|
@@ -1923,11 +2003,50 @@ the browser requires), `protocols`, `requireUnreliable`, `congestionControl`,
|
|
|
1923
2003
|
`session` (a TLS session from an earlier `'session'` event, to resume — not with
|
|
1924
2004
|
`serverCertificateHashes`: a resumed handshake has no certificate to pin),
|
|
1925
2005
|
`initialCongestionWindow` (as the server's),
|
|
1926
|
-
`allowPooling
|
|
1927
|
-
`{ streamErrorCode }` form as well as the spec's `(message, options)`. A test runs the same
|
|
2006
|
+
`allowPooling`, the `anticipatedConcurrent…` hints and `datagramsReadableType`. A test runs the same
|
|
1928
2007
|
client function in Chromium and in Node against a QUICO server and checks both see the same.
|
|
1929
2008
|
`sendOrder` and `sendGroup` order what the client sends, as on the server (see above).
|
|
1930
2009
|
|
|
2010
|
+
**The browser's interfaces** are exported, for `instanceof` as in the browser:
|
|
2011
|
+
`WebTransportBidirectionalStream`, `WebTransportSendStream`, `WebTransportReceiveStream`,
|
|
2012
|
+
`WebTransportWriter`, `WebTransportSendGroup`, `WebTransportDatagramsWritable`,
|
|
2013
|
+
`WebTransportDatagramDuplexStream`. As in the browser, none of them can be constructed by
|
|
2014
|
+
the application. `WebTransportError` is a `DOMException` (its `name` is
|
|
2015
|
+
`'WebTransportError'`, with `source` and `streamErrorCode`) and takes the browser's
|
|
2016
|
+
`{ message, source, streamErrorCode }` form as well as the spec's `(message, options)`. Where
|
|
2017
|
+
QUICO gives a Node code too (`ERR_WEBTRANSPORT_SESSION_LOST`, say), it is in `err.code`, in
|
|
2018
|
+
place of the `DOMException`'s number.
|
|
2019
|
+
|
|
2020
|
+
| | |
|
|
2021
|
+
|---|---|
|
|
2022
|
+
| `wt.responseHeaders` | The server's answer to the CONNECT: a `Headers` that cannot be changed, `null` until `ready`. No pseudo-headers and no `wt-protocol` (that is `wt.protocol`). `set-cookie` is there (`getSetCookie()`): a browser keeps it from its page, but in Node the program is the user agent, as with Node's `fetch`. |
|
|
2023
|
+
| `writer.atomicWrite(chunk)` | `getWriter()` of a send stream gives a `WebTransportWriter`. An atomic write goes only if all of it fits the flow-control window when its turn comes; otherwise none of it is sent and it rejects with an `AbortError`, as does any atomic write queued behind it, and the stream goes on. A plain write queued behind it fails the stream, as the spec has it. |
|
|
2024
|
+
| `writer.commit()` | What was written so far is delivered even if the stream is aborted later: `abort()` then sends RESET_STREAM_AT with that many bytes reliable (a MOQT subgroup's header, say) — `stream.resetStream(code, { reliableSize })` in the Node API. A peer that does not take RESET_STREAM_AT gets a plain RESET_STREAM. |
|
|
2025
|
+
| `readable.getReader({ mode: 'byob' })` | Streams are byte streams, as the browser's: a BYOB reader reads into its own buffer, and what does not fit stays for the next read. Datagrams too, with `datagramsReadableType: 'bytes'`: each datagram whole into the reader's buffer, a `RangeError` if it does not fit. |
|
|
2026
|
+
|
|
2027
|
+
It behaves as the browser's does where the web-platform-tests check it:
|
|
2028
|
+
|
|
2029
|
+
- **The constructor checks its arguments.** A URL that does not parse, is not `https:`, or
|
|
2030
|
+
has a fragment is a `SyntaxError` (a `DOMException`, as Node's own `WebSocket` throws), and
|
|
2031
|
+
so is a protocol in `protocols` that is empty, longer than 512, not printable ASCII, or
|
|
2032
|
+
given twice. A malformed `serverCertificateHashes` entry, a `congestionControl` that is
|
|
2033
|
+
not one of the three, or an `anticipatedConcurrent…` hint outside 0–65535 is a
|
|
2034
|
+
`TypeError`. The hints are attributes that can be set later, with the same checks.
|
|
2035
|
+
- **Streams and datagrams can be used before `ready`.** `create…Stream()` waits for the
|
|
2036
|
+
session and then resolves; a datagram written meanwhile goes once the session is up.
|
|
2037
|
+
- **The Promises reject as the browser's do.** `create…Stream()` on a session that is over
|
|
2038
|
+
rejects with an `InvalidStateError`, a send group of another session's with a
|
|
2039
|
+
`TypeError`, and a stream past the server's limit without `waitUntilAvailable` with a
|
|
2040
|
+
`QuotaExceededError`. `getStats()` on a session that failed rejects with an
|
|
2041
|
+
`InvalidStateError`.
|
|
2042
|
+
- **A stream fails as soon as its direction is given up on.** The server's `STOP_SENDING`
|
|
2043
|
+
rejects `writer.closed`, its `RESET_STREAM` rejects `reader.closed`, and the session's
|
|
2044
|
+
end fails both, without waiting for the next read or write. A stream write that is not
|
|
2045
|
+
an `ArrayBuffer` or a view onto one, or is empty, rejects with a `TypeError`.
|
|
2046
|
+
- **The anticipated-streams hints take effect.** Given to the constructor, they raise the
|
|
2047
|
+
stream limits the client grants the server from the start, unless `transportParams`
|
|
2048
|
+
sets those limits itself.
|
|
2049
|
+
|
|
1931
2050
|
The same object also has a Node-style API, for Node code:
|
|
1932
2051
|
|
|
1933
2052
|
| | |
|
|
@@ -1953,7 +2072,45 @@ platform roots unless `ca` is given or `serverCertificateHashes` pins it. If the
|
|
|
1953
2072
|
cannot be established, `ready` and `closed` reject with a `WebTransportError` (source
|
|
1954
2073
|
`'session'`, keeping node:tls's `err.code`, e.g. `DEPTH_ZERO_SELF_SIGNED_CERT`, when TLS is
|
|
1955
2074
|
the reason), `state` becomes `'failed'` and the connection is closed, as in the browser.
|
|
1956
|
-
`'error'` is emitted too, but only when a listener is attached.
|
|
2075
|
+
`'error'` is emitted too, but only when a listener is attached. Any `2xx` answer to the
|
|
2076
|
+
CONNECT establishes the session, as in the browser (draft-ietf-webtrans-http3 §3.3), not
|
|
2077
|
+
only `200`.
|
|
2078
|
+
|
|
2079
|
+
**Fields on the CONNECT request.** A browser always sends its page's `Origin`, and servers
|
|
2080
|
+
check it (the web-platform-tests WebTransport server refuses a session without one). Node has
|
|
2081
|
+
no page, so the client takes `origin`, and `headers` for other fields, as QUICO's WebSocket
|
|
2082
|
+
client does:
|
|
2083
|
+
|
|
2084
|
+
```js
|
|
2085
|
+
new WebTransport(url, { origin: 'https://app.example', headers: { authorization: 'Bearer …' } });
|
|
2086
|
+
```
|
|
2087
|
+
|
|
2088
|
+
`headers` is the browser's `HeadersInit`: a `Headers`, a list of `[name, value]` pairs, or
|
|
2089
|
+
an object. The fields go after the pseudo-headers and are treated as the browser's
|
|
2090
|
+
`Headers` treats them: names are lower-cased, leading and trailing whitespace is removed
|
|
2091
|
+
from values, and a name given twice is sent once with its values joined by `, `.
|
|
2092
|
+
|
|
2093
|
+
| Field | What happens |
|
|
2094
|
+
|---|---|
|
|
2095
|
+
| A pseudo-header, `wt-available-protocols` (that is `protocols`), a name or value that is not valid | The constructor throws a `TypeError`, as the browser's does |
|
|
2096
|
+
| `host`, `te`, `content-length`, `trailer`, connection-specific fields (`connection`, `keep-alive`, `transfer-encoding`, `upgrade`, …) | Dropped, as the browser drops them: they cannot go on an HTTP/3 request (the authority is `:authority`) |
|
|
2097
|
+
| `cookie`, `referer`, `sec-*` and the rest | Sent. A browser keeps them from a page for the page's safety; in Node the program is the user agent, as with Node's `fetch` |
|
|
2098
|
+
|
|
2099
|
+
##### How the session ends
|
|
2100
|
+
|
|
2101
|
+
| | `closed` | `'close'` |
|
|
2102
|
+
|---|---|---|
|
|
2103
|
+
| Either side sends `CLOSE_WEBTRANSPORT_SESSION` (`wt.close({ closeCode, reason })`, `res.closeSession(code, reason)`) | resolves with `{ closeCode, reason }` | emitted |
|
|
2104
|
+
| The server ends the CONNECT stream with a FIN and no capsule | resolves with `{ closeCode: 0, reason: '' }`, at once (draft-ietf-webtrans-http3 §5) | emitted |
|
|
2105
|
+
| The session is lost: the server resets the CONNECT stream, or the connection goes (an error, the idle timeout, the server gone) | **rejects** with a `WebTransportError` (source `'session'`, `code` `'ERR_WEBTRANSPORT_SESSION_LOST'`, and `quicError` `{ code, application, reason }` when the peer closed the connection), as soon as the server's CONNECTION_CLOSE arrives, not after QUIC's draining period; `state` is `'failed'` | emitted |
|
|
2106
|
+
| The session could not be established, or `wt.close()` was called before `ready` | rejects, with the same error as `ready` | emitted |
|
|
2107
|
+
|
|
2108
|
+
As in the browser, `{ closeCode: 0 }` means a close someone asked for; a session that was
|
|
2109
|
+
lost rejects instead. The session's streams, its incoming-stream queues and its datagrams
|
|
2110
|
+
fail with it. When the session was lost or never established, they fail with the very
|
|
2111
|
+
error object `closed` rejects with. After our own `close()` they fail with an
|
|
2112
|
+
`AbortError`, and after the server's with a `WebTransportError`. A clean close closes the
|
|
2113
|
+
incoming-stream queues and the datagrams' `readable` instead of failing them.
|
|
1957
2114
|
|
|
1958
2115
|
#### Client (Browser)
|
|
1959
2116
|
|
|
@@ -1964,7 +2121,7 @@ needed:
|
|
|
1964
2121
|
const wt = new WebTransport('https://yourserver.com:4433/live');
|
|
1965
2122
|
await wt.ready;
|
|
1966
2123
|
|
|
1967
|
-
const writer = wt.datagrams.
|
|
2124
|
+
const writer = wt.datagrams.createWritable().getWriter();
|
|
1968
2125
|
await writer.write(new TextEncoder().encode('Hello QUIC'));
|
|
1969
2126
|
|
|
1970
2127
|
const reader = wt.datagrams.readable.getReader();
|
|
@@ -2026,7 +2183,58 @@ The client connects over HTTP/1.1. Options beyond the WHATWG constructor: `origi
|
|
|
2026
2183
|
`headers`, `ca`, `rejectUnauthorized`, `maxPayload`, `agent` (whose TLS sessions a
|
|
2027
2184
|
`wss://` connection resumes with and keeps — `globalAgent` by default) and `session` (one
|
|
2028
2185
|
to resume with, from an earlier `'session'`). `ws.on('session', buf)`: each one the server
|
|
2029
|
-
sends.
|
|
2186
|
+
sends. The second argument may also be the browser's options bag: `new WebSocket(url, { protocols: ['chat'] })`.
|
|
2187
|
+
|
|
2188
|
+
It behaves as a browser's does (WHATWG, WPT `websockets/`):
|
|
2189
|
+
|
|
2190
|
+
- Messages go in the order `send()` was called — a `Blob` included, which is read first.
|
|
2191
|
+
- `close(code, reason)` checks as the browser: `null` is 0 and a code is clamped to 16 bits, so
|
|
2192
|
+
anything but 1000 or 3000–4999 is an `InvalidAccessError`; a reason over 123 bytes, a
|
|
2193
|
+
`SyntaxError`. `close()` while connecting abandons the handshake: `'error'`, then `'close'`
|
|
2194
|
+
1006, both on a later turn.
|
|
2195
|
+
- A URL with a fragment (even an empty `#`), or protocols repeated in another case
|
|
2196
|
+
(`['chat', 'CHAT']`): `SyntaxError`.
|
|
2197
|
+
- A port browsers refuse (Fetch's bad ports: 22, 25, 6000, …): never connected — `'error'`,
|
|
2198
|
+
then `'close'` 1006.
|
|
2199
|
+
- User and password in the URL (`wss://user:pass@host/`): sent as `Authorization: Basic`,
|
|
2200
|
+
unless `headers` has one.
|
|
2201
|
+
- `MessageEvent.origin` is the URL's origin; `send()` after close adds to `bufferedAmount`.
|
|
2202
|
+
|
|
2203
|
+
#### WebSocketStream (experimental)
|
|
2204
|
+
|
|
2205
|
+
The browser's streams-based WebSocket ([explainer](https://github.com/ricea/websocketstream-explainer);
|
|
2206
|
+
Chrome ships it, not yet a standard — the API may still change). The same connection, but a
|
|
2207
|
+
`ReadableStream` and a `WritableStream` in place of events, so both directions have backpressure:
|
|
2208
|
+
|
|
2209
|
+
```js
|
|
2210
|
+
import { WebSocketStream, WebSocketError } from 'quico';
|
|
2211
|
+
|
|
2212
|
+
const wss = new WebSocketStream('wss://localhost:4433/chat',
|
|
2213
|
+
{ protocols: ['chat'], signal: AbortSignal.timeout(5000), rejectUnauthorized: false });
|
|
2214
|
+
const { readable, writable, protocol } = await wss.opened;
|
|
2215
|
+
|
|
2216
|
+
const writer = writable.getWriter();
|
|
2217
|
+
await writer.write('hello'); // resolves once the frame is sent
|
|
2218
|
+
for await (const message of readable) { // a string, or a Uint8Array
|
|
2219
|
+
console.log(message);
|
|
2220
|
+
}
|
|
2221
|
+
const { closeCode, reason } = await wss.closed;
|
|
2222
|
+
```
|
|
2223
|
+
|
|
2224
|
+
- **Receiving.** A message nobody has read holds the socket: it is paused, TCP fills up, and the
|
|
2225
|
+
server's sends wait. With `WebSocket`, every message arrives as an event, however fast, into memory.
|
|
2226
|
+
- **Sending.** `write()` resolves when the frame has gone to the socket — a writer that awaits it
|
|
2227
|
+
goes as fast as the connection, with nothing piling up. `WebSocket`'s `send()` returns at once.
|
|
2228
|
+
- `close({ closeCode, reason })` — the checks of `WebSocket.close()`; a reason with no code is 1000.
|
|
2229
|
+
`closed` resolves with the code and reason the peer closed with.
|
|
2230
|
+
- A connection lost, or a close that left writes unsent: `closed` rejects with a `WebSocketError`
|
|
2231
|
+
(a `DOMException` with `closeCode` — 1006 when there was no closing handshake — and `reason`),
|
|
2232
|
+
and the readable and writable error with it.
|
|
2233
|
+
- `readable.cancel(err)` / `writable.abort(err)` close with the code and reason of a `WebSocketError`
|
|
2234
|
+
given: `new WebSocketError('', { closeCode: 4000, reason: 'bye' })`.
|
|
2235
|
+
- `signal` aborted before the connection is open: it is abandoned, `opened` and `closed` reject with
|
|
2236
|
+
the signal's reason; once open, it does nothing.
|
|
2237
|
+
- Options besides `protocols` and `signal` are `WebSocket`'s (`origin`, `headers`, `ca`, …).
|
|
2030
2238
|
|
|
2031
2239
|
---
|
|
2032
2240
|
|
|
@@ -2250,10 +2458,11 @@ For non-HTTP protocols over QUIC, or full control of the connection.
|
|
|
2250
2458
|
| `handshakeLoadThreshold`, `maxHandshakesPerAddress` | `number` | `500`, `64` | As `createServer`; `getPendingHandshakes()`, `setOptions()` / `getOptions()` too |
|
|
2251
2459
|
| `pmtuDiscovery`, `maxUdpPayloadSize`, `initialRtt`, `reorderWindow` | | | As `createServer` |
|
|
2252
2460
|
| `resetTokenSecret` | `Buffer` | random | Stateless-reset secret |
|
|
2461
|
+
| `tokenSecret` | `Buffer` (≥ 16 bytes) | random | Address-validation token secret (Retry, NEW_TOKEN), as `createServer` |
|
|
2253
2462
|
| `socket` / `socket6` | `dgram.Socket` | `null` | External, already-bound UDP sockets |
|
|
2254
2463
|
|
|
2255
2464
|
Returns `{ listen, close, on, handlePacket, hasConnection }` plus the runtime getters and
|
|
2256
|
-
setters for ticket keys, address validation and the
|
|
2465
|
+
setters for ticket keys, address validation, the reset secret and the token secret. The `'connection'`
|
|
2257
2466
|
event fires with `(quicConnection, peer)`.
|
|
2258
2467
|
|
|
2259
2468
|
```js
|
|
@@ -2274,7 +2483,7 @@ qs.on('connection', (quic, peer) => {
|
|
|
2274
2483
|
qs.listen(4433);
|
|
2275
2484
|
```
|
|
2276
2485
|
|
|
2277
|
-
`handlePacket(msg, rinfo)` and `hasConnection(rinfo)` support **external socket
|
|
2486
|
+
`handlePacket(msg, rinfo)` and `hasConnection(rinfo[, msg])` support **external socket
|
|
2278
2487
|
mode**, where QUICO shares a UDP port with another protocol (STUN/TURN, for example)
|
|
2279
2488
|
and you route datagrams yourself.
|
|
2280
2489
|
|
|
@@ -2305,6 +2514,10 @@ and you route datagrams yourself.
|
|
|
2305
2514
|
finalSize`), `'stop_sending'` (`id, code`), `'datagram'`, `'path'` (`address, port`),
|
|
2306
2515
|
`'peer_transport_params'`, `'close'`, `'error'`, `'keylog'`; a client's 0-RTT: `'early'` (it may
|
|
2307
2516
|
send application data now), `'early_accepted'`, `'early_rejected'` (what went early is forgotten — send it again).
|
|
2517
|
+
`'closing'` (`peerClose` — `{ error, application, reason }` when the peer's CONNECTION_CLOSE
|
|
2518
|
+
ended it, `null` when we did) fires once, the moment the connection is over for the
|
|
2519
|
+
application; `'close'` follows when the closing or draining period ends (RFC 9000 §10.2, up to
|
|
2520
|
+
3 s), which is packet housekeeping. Code that reacts to the end belongs on `'closing'`.
|
|
2308
2521
|
|
|
2309
2522
|
**Connection options** (when constructing directly): `idleTimeout` (default `30000`),
|
|
2310
2523
|
`handshakeTimeout` (default `10000`), `keepAlive` (`true` → half the idle timeout, or
|
|
@@ -2470,6 +2683,7 @@ quico/
|
|
|
2470
2683
|
├── ws.js — WebSocket framing, and the WHATWG and ws-style event surfaces
|
|
2471
2684
|
├── ws_server.js — WebSocketServer, upgradeWebSocket
|
|
2472
2685
|
├── ws_client.js — WebSocket client
|
|
2686
|
+
├── ws_stream.js — WebSocketStream, WebSocketError (experimental)
|
|
2473
2687
|
│
|
|
2474
2688
|
├── ja4h.js — JA4H request fingerprint
|
|
2475
2689
|
├── h2_fingerprint.js — HTTP/2 and HTTP/3 fingerprints
|
|
@@ -2595,9 +2809,9 @@ with the error the RFC names, not left to grow.
|
|
|
2595
2809
|
parameters, packet headers, HTTP/1.1, WebSocket, HPACK and HTTP/2 connections, QPACK,
|
|
2596
2810
|
capsules — and HTTP/3 and QUIC frames on live connections, with mutated valid input and
|
|
2597
2811
|
noise; none ends in an exception of its own, a hang, or an internal-error close.
|
|
2598
|
-
- **Closing says so**: `server.close()`
|
|
2599
|
-
and closes the socket only once those have left, so
|
|
2600
|
-
by their idle timeout.
|
|
2812
|
+
- **Closing says so**: `server.close()` lets requests in flight finish, then sends each
|
|
2813
|
+
connection its `CONNECTION_CLOSE` and closes the socket only once those have left, so
|
|
2814
|
+
clients learn at once rather than by their idle timeout.
|
|
2601
2815
|
|
|
2602
2816
|
To report a vulnerability, please see [SECURITY.md](./SECURITY.md) — or write to
|
|
2603
2817
|
**support@quicojs.dev** — rather than opening a public issue.
|