@amritk/nish-x86_64-linux 0.16.0 → 0.18.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.
Files changed (58) hide show
  1. package/INSTALL.md +43 -34
  2. package/bin/nish +0 -0
  3. package/package.json +1 -1
  4. package/runtime/nish.d.ts +117 -8
  5. package/runtime/nish.h +50 -13
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +43 -24
  8. package/runtime/runtime-net.c +57 -10
  9. package/runtime/runtime-os.c +62 -20
  10. package/runtime/runtime-simd.c +203 -0
  11. package/runtime/runtime-wasm.c +12 -0
  12. package/runtime/runtime.c +66 -18
  13. package/runtime/shim.mjs +114 -5
  14. package/scripts/build.sh +14 -12
  15. package/std/README.md +168 -36
  16. package/std/crypto/aes.ts +51 -8
  17. package/std/crypto/base64url.ts +25 -12
  18. package/std/crypto/chacha20poly1305.ts +43 -29
  19. package/std/crypto/ct.ts +6 -1
  20. package/std/crypto/hkdf.ts +364 -3
  21. package/std/crypto/hmac.ts +307 -7
  22. package/std/crypto/p256.ts +618 -34
  23. package/std/crypto/sha1.ts +147 -0
  24. package/std/crypto/sha256.ts +3 -2
  25. package/std/crypto/x25519.ts +88 -36
  26. package/std/crypto/x509.ts +130 -47
  27. package/std/json.ts +297 -41
  28. package/std/net/hpack.ts +1094 -0
  29. package/std/net/http-fields.ts +604 -0
  30. package/std/net/http1-server.ts +1389 -0
  31. package/std/net/http1.ts +1492 -0
  32. package/std/net/http2-frame.ts +745 -0
  33. package/std/net/http2-tls.ts +216 -0
  34. package/std/net/http2.ts +1433 -0
  35. package/std/net/http3-frame.ts +457 -0
  36. package/std/net/http3-server.ts +911 -0
  37. package/std/net/http3.ts +2145 -0
  38. package/std/net/qpack.ts +1163 -0
  39. package/std/net/quic-conn-ack.ts +206 -0
  40. package/std/net/quic-conn-cid.ts +425 -0
  41. package/std/net/quic-conn-params.ts +462 -0
  42. package/std/net/quic-datagram.ts +130 -0
  43. package/std/net/quic-frame.ts +1206 -0
  44. package/std/net/quic-listener.ts +929 -0
  45. package/std/net/quic-packet.ts +1698 -0
  46. package/std/net/quic-recovery.ts +849 -0
  47. package/std/net/quic-stream.ts +1609 -0
  48. package/std/net/quic.ts +3578 -0
  49. package/std/net/tls/codec.ts +1089 -0
  50. package/std/net/tls/record-server.ts +1173 -0
  51. package/std/net/tls/record.ts +621 -0
  52. package/std/net/tls/schedule.ts +429 -0
  53. package/std/net/tls-tcp.ts +414 -0
  54. package/std/net/tls.ts +1437 -0
  55. package/std/net/websocket.ts +823 -0
  56. package/std/net/webtransport.ts +1334 -0
  57. package/std/secret.ts +62 -0
  58. package/std/text.ts +77 -0
@@ -0,0 +1,1334 @@
1
+ /**
2
+ * `nish/net/webtransport` — WebTransport over HTTP/3, server side, as
3
+ * draft-ietf-webtrans-http3-02 has it and as Chrome and the `wtransport` 0.7
4
+ * crate negotiate it: sessions made by an extended CONNECT (RFC 9220) with
5
+ * `:protocol` `webtransport`, HTTP datagrams (RFC 9297) carrying the
6
+ * session's quarter stream ID, the session's unidirectional and
7
+ * bidirectional streams, and CLOSE_WEBTRANSPORT_SESSION and
8
+ * DRAIN_WEBTRANSPORT_SESSION on the CONNECT stream's capsules. It is a layer
9
+ * over one `Http3Connection` (`nish/net/http3`), whose `Http3Config` turns
10
+ * WebTransport on with `webtransportSessions`, and is sans-IO like it.
11
+ *
12
+ * import { WebTransport, WebTransportConfig, WT_SESSION, WT_DATAGRAM } from "nish/net/webtransport";
13
+ *
14
+ * const wt = new WebTransport(new WebTransportConfig(), h3); // once per slot, beside its Http3Connection
15
+ * // after quic.receive(...):
16
+ * let event: i32 = wt.next();
17
+ * while (event !== H3_NEED_MORE && event !== H3_ERROR) {
18
+ * // WT_SESSION: wt.sessionId, wt.fields (path, get("origin")) — accept or refuse it
19
+ * // WT_DATAGRAM: wt.sessionId, wt.data[wt.dataStart .. wt.dataStart + wt.dataLength)
20
+ * if (event === WT_SESSION) { wt.accept(wt.sessionId); }
21
+ * if (event === WT_DATAGRAM) { wt.sendDatagram(wt.sessionId, wt.data, wt.dataStart, wt.dataLength); }
22
+ * event = wt.next();
23
+ * }
24
+ *
25
+ * **Events.** `next()` answers a `WT_*` event, or one of `nish/net/http3`'s
26
+ * for a plain request, which passes through untouched so one connection can
27
+ * serve both: `WT_SESSION` (a session asked for: `sessionId`, the CONNECT
28
+ * stream's ID, and `fields`; the program `accept`s or `refuse`s it),
29
+ * `WT_DATAGRAM` (a datagram of an accepted session, a window onto `data`),
30
+ * `WT_STREAM` (the client opened a stream of an accepted session: `stream`,
31
+ * `bidirectional`), `WT_STREAM_DATA` (its bytes, a window onto `data`),
32
+ * `WT_STREAM_END` (its FIN), `WT_STREAM_RESET` (the client reset its side:
33
+ * `errorCode`, the HTTP/3 code, and `appCode`, the WebTransport code it maps
34
+ * to or -1), `WT_STREAM_STOPPED` (the client asked this side to stop writing:
35
+ * the same two codes), `WT_WRITABLE` (a stream, or a CONNECT stream whose
36
+ * capsule was held back, can take more), `WT_DRAIN` (the client sent
37
+ * DRAIN_WEBTRANSPORT_SESSION) and `WT_CLOSED` (the session is over:
38
+ * `errorCode`, the CLOSE capsule's code or 0, the reason a window onto
39
+ * `reason`, and `closedByPeer`). Each event names its session in `sessionId`
40
+ * and in `session`, its index in the table, `0 .. webtransportSessions - 1`,
41
+ * for a program's own per-session arrays. A window is valid until the next
42
+ * call.
43
+ *
44
+ * **Writes.** `sendDatagram` (the quarter stream ID, then the payload, one
45
+ * QUIC DATAGRAM frame; `maxDatagramPayload` says how much fits), `openStream`,
46
+ * `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION
47
+ * and the FIN) and `drain`. Each answers 0 or a count, or one of
48
+ * `nish/net/http3`'s codes: H3_AGAIN, H3_CLOSED, H3_INVALID, H3_TOO_LARGE,
49
+ * and `WT_LIMIT` for a stream past the session's cap.
50
+ *
51
+ * **A session** is the CONNECT stream's ID (draft-02 §2). A request is a
52
+ * session when its `:protocol` is `webtransport`, its `:scheme` `https`, and
53
+ * the client's SETTINGS offered SETTINGS_H3_DATAGRAM 1 and either
54
+ * SETTINGS_ENABLE_WEBTRANSPORT 1 or SETTINGS_WEBTRANSPORT_MAX_SESSIONS
55
+ * (§3.1); otherwise it is answered here and never reaches the program — 501
56
+ * for another `:protocol` (RFC 8441 §4), 400 for the rest — as is one past
57
+ * the session cap, with 429. `accept` answers 200 with
58
+ * `sec-webtransport-http3-draft: draft02`, the response header Chrome's
59
+ * draft-02 client looks for. With WebTransport on, `nish/net/http3` holds a
60
+ * request until the client's SETTINGS have arrived (§3.1), and an extended
61
+ * CONNECT is malformed, H3_MESSAGE_ERROR, where this side did not send
62
+ * SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC 9220 §3).
63
+ *
64
+ * **Datagrams** (RFC 9297 §2.1). A datagram whose quarter stream ID does not
65
+ * parse, or names a stream past 2^62, is H3_DATAGRAM_ERROR and closes the
66
+ * connection; one for a session that is not accepted — unknown, gone, or not
67
+ * yet answered — is dropped and counted in `datagramsDropped`.
68
+ *
69
+ * **Streams** (§4). A stream names its session in its first bytes;
70
+ * `nish/net/http3` reads them and holds the stream. One for an accepted
71
+ * session is taken, up to `maxStreams` at once, and refused past it with
72
+ * H3_REQUEST_REJECTED; one for a session asked for but not answered, or one
73
+ * whose CONNECT stream has not arrived, waits — up to `maxPending` at once,
74
+ * unread, so QUIC's flow control holds the client back — and is refused with
75
+ * WT_BUFFERED_STREAM_REJECTED past that (§4.5); one for a session that is
76
+ * gone, refused or was never one is refused with WT_SESSION_GONE. Refusing a
77
+ * stream resets this side and asks the client to stop.
78
+ *
79
+ * **Capsules** (RFC 9297 §3) on the CONNECT stream's DATA: CLOSE (0x2843: a
80
+ * 32-bit code and a UTF-8 reason of at most 1024 bytes) and DRAIN (0x78ae,
81
+ * empty) are read; any other type is skipped. A CLOSE outside those lengths,
82
+ * a DRAIN with a payload, or the stream ending inside a capsule is
83
+ * H3_DATAGRAM_ERROR, and anything after a CLOSE is H3_MESSAGE_ERROR (§5);
84
+ * each resets the CONNECT stream and ends the session. A session ends when
85
+ * either side sends CLOSE, or the CONNECT stream ends or is reset: every one
86
+ * of its streams is then reset with WT_SESSION_GONE, and this side's half of
87
+ * the CONNECT stream finished.
88
+ *
89
+ * **Caps are start-up numbers**: the sessions (`Http3Config.webtransportSessions`),
90
+ * the streams of one session and the streams waiting for theirs
91
+ * (`WebTransportConfig`), and the datagram queues, which are the QUIC
92
+ * connection's rings (`QUIC_CONN_DATAGRAM_QUEUE` each way). Every array is
93
+ * made by the constructor; a session, a stream and a datagram find their
94
+ * state through the QUIC connection's stream index, never by a scan, and
95
+ * nothing a warm session does allocates. Counters a peer drives saturate.
96
+ *
97
+ * **Error codes.** A stream's reset and STOP_SENDING carry an HTTP/3 code;
98
+ * a WebTransport application's 32-bit code maps into the range from
99
+ * 0x52e4a40fa8db (`wtCodeToHttp3`, `wtCodeFromHttp3`), as Chrome maps it.
100
+ * `wtransport` 0.7 sends its codes unmapped, so an event carries both.
101
+ *
102
+ * Private helpers share the importing program's flat symbol namespace
103
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
104
+ *
105
+ * Written from draft-ietf-webtrans-http3-02, RFC 9220 and RFC 9297, with the
106
+ * identifiers checked against `wtransport` 0.7.0 and Chrome's behaviour; no
107
+ * code is ported from either.
108
+ */
109
+ import { HttpFields, httpFieldBytes, httpFieldIs } from "nish/net/http-fields"
110
+ import {
111
+ H3_DATAGRAM_ERROR,
112
+ H3_MESSAGE_ERROR,
113
+ H3_NO_ERROR,
114
+ H3_REQUEST_REJECTED,
115
+ H3_WT_SESSION_GONE,
116
+ h3Count,
117
+ h3CheckWindow,
118
+ h3Greased,
119
+ h3PutFrameHeader,
120
+ h3PutVarint,
121
+ h3ReadFrameHeader,
122
+ h3ReadVarint,
123
+ h3VarintLength,
124
+ Http3FrameHeader,
125
+ } from "nish/net/http3-frame"
126
+ import {
127
+ H3_AGAIN,
128
+ H3_CLOSED,
129
+ H3_DATA,
130
+ H3_DROPPED,
131
+ H3_END,
132
+ H3_ERROR,
133
+ H3_GOAWAY,
134
+ H3_INVALID,
135
+ H3_NEED_MORE,
136
+ H3_REQUEST,
137
+ H3_RESET,
138
+ H3_STOPPED,
139
+ H3_STREAM,
140
+ H3_TOO_LARGE,
141
+ H3_WRITABLE,
142
+ Http3Connection,
143
+ } from "nish/net/http3"
144
+ import { QUIC_DATAGRAM_ERR_FULL, QUIC_DATAGRAM_NONE, QUIC_DATAGRAM_OK, QuicConnection } from "nish/net/quic"
145
+ import { quicPacketCopy, quicVarintSize } from "nish/net/quic-packet"
146
+ import { quicStreamIsUni } from "nish/net/quic-stream"
147
+
148
+ /** A session asked for: `sessionId`, `fields`. */
149
+ export const WT_SESSION: i32 = 16
150
+ /** A datagram of an accepted session: `data[dataStart .. dataStart + dataLength)`. */
151
+ export const WT_DATAGRAM: i32 = 17
152
+ /** The client opened a stream of an accepted session: `stream`, `bidirectional`. */
153
+ export const WT_STREAM: i32 = 18
154
+ /** A stream's bytes: `stream`, `data[dataStart .. dataStart + dataLength)`. */
155
+ export const WT_STREAM_DATA: i32 = 19
156
+ /** A stream's FIN: `stream`. */
157
+ export const WT_STREAM_END: i32 = 20
158
+ /** The client reset its side of `stream`: `errorCode`, `appCode`. */
159
+ export const WT_STREAM_RESET: i32 = 21
160
+ /** The client asked this side of `stream` to stop, and QUIC reset it: `errorCode`, `appCode`. */
161
+ export const WT_STREAM_STOPPED: i32 = 22
162
+ /** `stream` — a session's stream, or its CONNECT stream — can take more. */
163
+ export const WT_WRITABLE: i32 = 23
164
+ /** The session is over: `errorCode`, `reason[reasonStart .. reasonStart + reasonLength)`, `closedByPeer`. */
165
+ export const WT_CLOSED: i32 = 24
166
+ /** The client sent DRAIN_WEBTRANSPORT_SESSION: it will close the session soon. */
167
+ export const WT_DRAIN: i32 = 25
168
+
169
+ /** A stream past the session's `maxStreams`: Linux's EMFILE. */
170
+ export const WT_LIMIT: i32 = -24
171
+
172
+ /** CLOSE_WEBTRANSPORT_SESSION's capsule type (draft-02 §5). */
173
+ export const WT_CAPSULE_CLOSE: i64 = 0x2843
174
+ /** DRAIN_WEBTRANSPORT_SESSION's capsule type. */
175
+ export const WT_CAPSULE_DRAIN: i64 = 0x78ae
176
+ /** The longest reason a CLOSE capsule carries. */
177
+ export const WT_REASON_MAX: i32 = 1024
178
+ /** A stream of a session that is gone (draft-02 §4.5). */
179
+ export const WT_SESSION_GONE: i64 = H3_WT_SESSION_GONE
180
+ /** A stream refused because no more may wait for their session (§4.5). */
181
+ export const WT_BUFFERED_STREAM_REJECTED: i64 = 0x3994bd84
182
+ /** The first HTTP/3 code a WebTransport application code maps to. */
183
+ export const WT_FIRST_APP_CODE: i64 = 0x52e4a40fa8db
184
+
185
+ /** The most a configuration may set `maxStreams` and `maxPending` to. */
186
+ const WT_MAX_CAP: i32 = 4096
187
+
188
+ /** Session states. */
189
+ const WT_FREE: i32 = 0
190
+ const WT_ASKED: i32 = 1
191
+ const WT_OPEN: i32 = 2
192
+ const WT_GONE: i32 = 3
193
+
194
+ /** A stream's bits: its side here done, and the client's side done. */
195
+ const WT_SENT: i32 = 1
196
+ const WT_RECEIVED: i32 = 2
197
+
198
+ /** Each session's slice of `reason`: a CLOSE capsule's 32-bit code, then its reason. */
199
+ const WT_REASON_STRIDE: i32 = WT_REASON_MAX + 4
200
+
201
+ /** The bytes a session keeps for a capsule header being read. */
202
+ const WT_HEAD: i32 = 16
203
+
204
+ /** The largest quarter stream ID (RFC 9297 §2.1): a stream ID is under 2^62. */
205
+ const WT_QUARTER_MAX: i64 = 1073741824 * 1073741824 - 1
206
+
207
+ /** Typed constants, since a bare literal is an `f64` under `--number-mode f64`. */
208
+ const WT_ZERO: i32 = 0
209
+ const WT_ZERO64: i64 = 0
210
+ const WT_STATUS_OK: i32 = 200
211
+ const WT_ONE: i32 = 1
212
+ const WT_NONE: i32 = -1
213
+ const WT_NONE64: i64 = -1
214
+ const WT_U32: i64 = 0xffffffff
215
+ /** Knuth's multiplicative constant, 2^32 over the golden ratio: odd, so the product keeps every bit of the salted number. */
216
+ const WT_HASH_MULTIPLIER: i64 = 2654435761
217
+
218
+ /** The caps of a WebTransport layer, fixed when it is made. */
219
+ export class WebTransportConfig {
220
+ /** Streams one session may have at once, both kinds and both openers together. */
221
+ maxStreams: i32 = 64
222
+ /** Streams that may wait at once for a session not yet answered or not yet arrived (draft-02 §4.5). */
223
+ maxPending: i32 = 16
224
+ }
225
+
226
+ /** The HTTP/3 code an application's 32-bit `code` is sent as (`WT_FIRST_APP_CODE` + n + n / 0x1e, skipping the greased values). */
227
+ export const wtCodeToHttp3 = (code: i64): i64 => {
228
+ const n: i64 = code & WT_U32
229
+ return WT_FIRST_APP_CODE + n + n / toI64(0x1e)
230
+ }
231
+
232
+ /** The application code an HTTP/3 `code` carries, or -1 when it is outside the range, or one of its greased values. */
233
+ export const wtCodeFromHttp3 = (code: i64): i64 => {
234
+ const last: i64 = wtCodeToHttp3(WT_U32)
235
+ if (code < WT_FIRST_APP_CODE || code > last || h3Greased(code)) {
236
+ return WT_NONE64
237
+ }
238
+ const shifted: i64 = code - WT_FIRST_APP_CODE
239
+ return shifted - shifted / toI64(0x1f)
240
+ }
241
+
242
+ /** Panics unless `value` lies in `[low, high]`: a cap out of range is the program's mistake. */
243
+ const webtransportCheckCap = (what: string, value: i32, low: i32, high: i32): void => {
244
+ if (value < low || value > high) {
245
+ panic(`WebTransport: ${what} of ${value}, outside ${low} to ${high}`)
246
+ }
247
+ }
248
+
249
+ /**
250
+ * The streams waiting for a session (draft-02 §4.5): at most `size`, filed
251
+ * by the session ID they named in a hash keyed with bytes of entropy, each
252
+ * bucket a list in arrival order. A session's waiting streams, and whether a
253
+ * request's stream has any, are found by one probe and a walk of that
254
+ * bucket, never by a pass over the room, so a client choosing session IDs
255
+ * cannot make a request, an accept or a close cost the room's size.
256
+ */
257
+ export class WebTransportWaiting {
258
+ /** Entry `e`: the stream, the session it named, its bucket's neighbours; a free entry's `next` threads the free list. */
259
+ streams: i64[]
260
+ sessions: i64[]
261
+ next: i32[]
262
+ prev: i32[]
263
+ /** Each bucket's first and last entry, or -1. */
264
+ heads: i32[]
265
+ tails: i32[]
266
+ salt: i64 = 0
267
+ freeHead: i32 = -1
268
+ count: i32 = 0
269
+
270
+ constructor(size: i32) {
271
+ const n: i32 = size > 0 ? size : WT_ONE
272
+ this.streams = new Array<i64>(n)
273
+ this.sessions = new Array<i64>(n)
274
+ this.next = new Array<i32>(n)
275
+ this.prev = new Array<i32>(n)
276
+ let buckets: i32 = 2
277
+ while (buckets < n * 2) {
278
+ buckets = buckets * 2
279
+ }
280
+ this.heads = new Array<i32>(buckets)
281
+ this.tails = new Array<i32>(buckets)
282
+ const entropy: u8[] = new Array<u8>(4)
283
+ crypto.getRandomValues(entropy)
284
+ for (let j: i32 = 0; j < toI32(entropy.length); j++) {
285
+ this.salt = (this.salt << toI64(8)) | toI64(toI32(entropy[j]))
286
+ }
287
+ this.reset()
288
+ }
289
+
290
+ /** Empties the room. */
291
+ reset(): void {
292
+ this.heads.fill(WT_NONE)
293
+ this.tails.fill(WT_NONE)
294
+ const n: i32 = toI32(this.next.length)
295
+ for (let e: i32 = 0; e < n; e++) {
296
+ this.next[e] = e + 1 < n ? e + 1 : WT_NONE
297
+ }
298
+ this.freeHead = 0
299
+ this.count = 0
300
+ }
301
+
302
+ /** The bucket of session ID `session`: its stream number, salted, times an odd constant, high bits. */
303
+ bucket(session: i64): i32 {
304
+ const x: i64 = ((session >> toI64(2)) ^ this.salt) & toI64(0x7fffffff)
305
+ const mixed: i64 = (x * WT_HASH_MULTIPLIER) >> toI64(16)
306
+ return toI32(mixed & toI64(toI32(this.heads.length) - 1))
307
+ }
308
+
309
+ /** Files stream `stream`, waiting for session `session`; answers whether there was room. */
310
+ add(stream: i64, session: i64): boolean {
311
+ const e: i32 = this.freeHead
312
+ if (e < 0 || e >= toI32(this.next.length)) {
313
+ return false
314
+ }
315
+ this.freeHead = this.next[e]
316
+ const b: i32 = this.bucket(session)
317
+ const tail: i32 = this.tails[b]
318
+ this.streams[e] = stream
319
+ this.sessions[e] = session
320
+ this.next[e] = -1
321
+ this.prev[e] = tail
322
+ if (tail >= 0 && tail < toI32(this.next.length)) {
323
+ this.next[tail] = e
324
+ } else {
325
+ this.heads[b] = e
326
+ }
327
+ this.tails[b] = e
328
+ this.count = this.count + 1
329
+ return true
330
+ }
331
+
332
+ /** The oldest entry waiting for session `session`, or -1. */
333
+ first(session: i64): i32 {
334
+ let e: i32 = this.heads[this.bucket(session)]
335
+ for (
336
+ let guard: i32 = 0;
337
+ guard < toI32(this.next.length) && e >= 0 && e < toI32(this.next.length);
338
+ guard++
339
+ ) {
340
+ if (this.sessions[e] === session) {
341
+ return e
342
+ }
343
+ e = this.next[e]
344
+ }
345
+ return WT_NONE
346
+ }
347
+
348
+ /** Takes entry `e` out of the room and answers its stream. */
349
+ take(e: i32): i64 {
350
+ const stream: i64 = this.streams[e]
351
+ const b: i32 = this.bucket(this.sessions[e])
352
+ const before: i32 = this.prev[e]
353
+ const after: i32 = this.next[e]
354
+ if (before >= 0 && before < toI32(this.next.length)) {
355
+ this.next[before] = after
356
+ } else {
357
+ this.heads[b] = after
358
+ }
359
+ if (after >= 0 && after < toI32(this.prev.length)) {
360
+ this.prev[after] = before
361
+ } else {
362
+ this.tails[b] = before
363
+ }
364
+ this.next[e] = this.freeHead
365
+ this.freeHead = e
366
+ this.count = this.count - 1
367
+ return stream
368
+ }
369
+ }
370
+
371
+ /**
372
+ * The WebTransport sessions of one HTTP/3 connection. The fields after the
373
+ * tables are the last event's; the module comment says which event sets
374
+ * which.
375
+ */
376
+ export class WebTransport {
377
+ config: WebTransportConfig
378
+ h3: Http3Connection
379
+ quic: QuicConnection
380
+ /** The request fields of the last WT_SESSION: `h3.fields`. */
381
+ fields: HttpFields
382
+ header: Http3FrameHeader
383
+ /** A datagram read whole, and one being sent; a CLOSE capsule being written. */
384
+ datagram: u8[]
385
+ outgoing: u8[]
386
+ capsuleOut: u8[]
387
+ /** The window of WT_DATAGRAM, WT_STREAM_DATA and WT_CLOSED: `datagram`, `h3.data`, or `reason`. */
388
+ data: u8[]
389
+ /** Each session's CLOSE payload as it arrives, `WT_REASON_MAX + 4` bytes a session. */
390
+ reason: u8[]
391
+ /** The response fields of an accepted session, and of a refusal. */
392
+ acceptNames: u8[][]
393
+ acceptValues: u8[][]
394
+ none: u8[][]
395
+ // Per session.
396
+ sessionIds: i64[]
397
+ /** The capsule being read: its type, and what is left of its payload (-1 between capsules). */
398
+ capType: i64[]
399
+ capLeft: i64[]
400
+ states: i32[]
401
+ /** The CONNECT stream's QUIC slot. */
402
+ sessionSlot: i32[]
403
+ /** The first of its streams (a QUIC slot, threaded through `nextStream`), and how many. */
404
+ firstStream: i32[]
405
+ streamCount: i32[]
406
+ /** Whether the client's CLOSE has been read: nothing may follow it. */
407
+ closeRead: boolean[]
408
+ /** A capsule header being read, `capHeadLen[s]` bytes of `capHead[s * 16 ..]`, and the CLOSE payload's fill. */
409
+ capHeadLen: i32[]
410
+ capHead: u8[]
411
+ capFill: i32[]
412
+ /** Counters for a log or a rate cap, each saturating: datagrams in and out. */
413
+ datagramsIn: i32[]
414
+ datagramsOut: i32[]
415
+ /** The free sessions, a stack. */
416
+ free: i32[]
417
+ // Per QUIC stream slot.
418
+ /** The session whose CONNECT stream the slot holds, or -1. */
419
+ sessionAt: i32[]
420
+ /** The stream the slot's stream state is for, its session, its neighbours in that session's list, and its bits. */
421
+ streamIds: i64[]
422
+ streamSession: i32[]
423
+ nextStream: i32[]
424
+ prevStream: i32[]
425
+ streamBits: i32[]
426
+ /** The streams waiting for a session. */
427
+ waiting: WebTransportWaiting
428
+ /** Sessions accepted while streams waited for them, `adoptCount` of them: `next` takes those streams first. */
429
+ adoptions: i32[]
430
+ /** The last event's session ID, stream, and codes. */
431
+ sessionId: i64 = -1
432
+ stream: i64 = -1
433
+ errorCode: i64 = 0
434
+ appCode: i64 = -1
435
+ /** Where the capsules of a DATA window are being read: `h3.data[capAt .. capEnd)`, for session `capSession`. */
436
+ capAt: i32 = 0
437
+ capEnd: i32 = 0
438
+ capSession: i32 = -1
439
+ /** The last event's session index, and its window onto `data`. */
440
+ session: i32 = -1
441
+ dataStart: i32 = 0
442
+ dataLength: i32 = 0
443
+ freeCount: i32 = 0
444
+ adoptCount: i32 = 0
445
+ /** The `h3.generation` this layer's state belongs to. */
446
+ generation: i32 = 0
447
+ /** Counters for a log or a test, each saturating: datagrams dropped, sessions refused here, streams refused. */
448
+ datagramsDropped: i32 = 0
449
+ sessionsRefused: i32 = 0
450
+ streamsRefused: i32 = 0
451
+ /** WT_STREAM's kind, and whether WT_CLOSED came from the client. */
452
+ bidirectional: boolean = false
453
+ closedByPeer: boolean = false
454
+
455
+ /**
456
+ * The WebTransport layer of `h3`, whose `Http3Config` must have
457
+ * `webtransportSessions` above 0. A cap out of range panics.
458
+ */
459
+ constructor(config: WebTransportConfig, h3: Http3Connection) {
460
+ const sessions: i32 = h3.config.webtransportSessions
461
+ // `Http3Connection` already holds the count to `H3_MAX_SESSIONS`; what can be wrong here is a connection with WebTransport off.
462
+ if (sessions < 1) {
463
+ panic("WebTransport: the Http3Connection has WebTransport off (webtransportSessions 0)")
464
+ }
465
+ webtransportCheckCap("maxStreams", config.maxStreams, 1, WT_MAX_CAP)
466
+ webtransportCheckCap("maxPending", config.maxPending, 0, WT_MAX_CAP)
467
+ this.config = config
468
+ this.h3 = h3
469
+ this.quic = h3.quic
470
+ this.fields = h3.fields
471
+ this.header = new Http3FrameHeader()
472
+ const entry: i32 = h3.quic.datagramsIn.entrySize
473
+ this.datagram = new Array<u8>(entry > 0 ? entry : WT_ONE)
474
+ this.outgoing = new Array<u8>(h3.quic.datagramsOut.entrySize + 8)
475
+ this.capsuleOut = new Array<u8>(WT_REASON_MAX + 4 + WT_HEAD)
476
+ this.data = this.datagram
477
+ this.reason = new Array<u8>(sessions * WT_REASON_STRIDE)
478
+ this.acceptNames = [httpFieldBytes("sec-webtransport-http3-draft")]
479
+ this.acceptValues = [httpFieldBytes("draft02")]
480
+ this.none = []
481
+ this.sessionIds = new Array<i64>(sessions)
482
+ this.capType = new Array<i64>(sessions)
483
+ this.capLeft = new Array<i64>(sessions)
484
+ this.states = new Array<i32>(sessions)
485
+ this.sessionSlot = new Array<i32>(sessions)
486
+ this.firstStream = new Array<i32>(sessions)
487
+ this.streamCount = new Array<i32>(sessions)
488
+ this.closeRead = new Array<boolean>(sessions)
489
+ this.capHeadLen = new Array<i32>(sessions)
490
+ this.capHead = new Array<u8>(sessions * WT_HEAD)
491
+ this.capFill = new Array<i32>(sessions)
492
+ this.datagramsIn = new Array<i32>(sessions)
493
+ this.datagramsOut = new Array<i32>(sessions)
494
+ this.free = new Array<i32>(sessions)
495
+ const n: i32 = toI32(h3.quic.streams.slots.length)
496
+ const slots: i32 = n > 0 ? n : WT_ONE
497
+ this.sessionAt = new Array<i32>(slots)
498
+ this.streamIds = new Array<i64>(slots)
499
+ this.streamSession = new Array<i32>(slots)
500
+ this.nextStream = new Array<i32>(slots)
501
+ this.prevStream = new Array<i32>(slots)
502
+ this.streamBits = new Array<i32>(slots)
503
+ this.waiting = new WebTransportWaiting(config.maxPending)
504
+ this.adoptions = new Array<i32>(sessions)
505
+ this.restart()
506
+ }
507
+
508
+ /** Forgets every session and stream, for the connection's next peer; `next` calls it when `h3` restarted. */
509
+ restart(): void {
510
+ this.generation = this.h3.generation
511
+ this.sessionIds.fill(WT_NONE64)
512
+ this.states.fill(WT_FREE)
513
+ this.sessionAt.fill(WT_NONE)
514
+ this.streamIds.fill(WT_NONE64)
515
+ this.streamSession.fill(WT_NONE)
516
+ this.freeCount = 0
517
+ for (let s: i32 = toI32(this.free.length) - 1; s >= 0; s--) {
518
+ this.free[this.freeCount] = s
519
+ this.freeCount = this.freeCount + 1
520
+ }
521
+ this.waiting.reset()
522
+ this.adoptCount = 0
523
+ this.capAt = 0
524
+ this.capEnd = 0
525
+ this.capSession = -1
526
+ this.sessionId = -1
527
+ this.stream = -1
528
+ this.session = -1
529
+ this.errorCode = 0
530
+ this.appCode = -1
531
+ this.dataStart = 0
532
+ this.dataLength = 0
533
+ this.datagramsDropped = 0
534
+ this.sessionsRefused = 0
535
+ this.streamsRefused = 0
536
+ this.bidirectional = false
537
+ this.closedByPeer = false
538
+ }
539
+
540
+ /** Restarts this layer when its connection restarted since the last call. */
541
+ current(): void {
542
+ if (this.generation !== this.h3.generation) {
543
+ this.restart()
544
+ }
545
+ }
546
+
547
+ // ---- Lookups ---------------------------------------------------------------------
548
+
549
+ /** The index of the session whose CONNECT stream is `id`, or -1: one probe of QUIC's stream index. */
550
+ sessionOf(id: i64): i32 {
551
+ const k: i32 = this.quic.streams.slotOf(id)
552
+ if (k < 0 || k >= toI32(this.sessionAt.length)) {
553
+ return WT_NONE
554
+ }
555
+ const s: i32 = this.sessionAt[k]
556
+ return s >= 0 && s < toI32(this.sessionIds.length) && this.sessionIds[s] === id ? s : WT_NONE
557
+ }
558
+
559
+ /** The QUIC slot of a session's stream `id` that this layer tracks, or -1. */
560
+ streamSlot(id: i64): i32 {
561
+ const k: i32 = this.quic.streams.slotOf(id)
562
+ if (k < 0 || k >= toI32(this.streamIds.length) || this.streamIds[k] !== id) {
563
+ return WT_NONE
564
+ }
565
+ return k
566
+ }
567
+
568
+ /** The session of `id` when it is accepted and not gone, else -1. */
569
+ openSession(id: i64): i32 {
570
+ this.current()
571
+ const s: i32 = this.sessionOf(id)
572
+ return s >= 0 && this.states[s] === WT_OPEN ? s : WT_NONE
573
+ }
574
+
575
+ // ---- Events ----------------------------------------------------------------------
576
+
577
+ /**
578
+ * Reads what the connection has and answers the next event (see the
579
+ * module comment): a `WT_*` one, or a plain request's `H3_*` one. Call it
580
+ * until it answers H3_NEED_MORE or H3_ERROR.
581
+ */
582
+ next(): i32 {
583
+ this.current()
584
+ for (let guard: i32 = 0; guard < 1000000; guard++) {
585
+ if (this.adoptCount > 0) {
586
+ const event: i32 = this.adoptWaiting()
587
+ if (event !== H3_NEED_MORE) {
588
+ return event
589
+ }
590
+ continue
591
+ }
592
+ if (this.capSession >= 0) {
593
+ const event: i32 = this.capsules()
594
+ if (event !== H3_NEED_MORE) {
595
+ return event
596
+ }
597
+ continue
598
+ }
599
+ const got: i32 = this.receiveDatagram()
600
+ if (got === WT_DATAGRAM) {
601
+ return got
602
+ }
603
+ if (got === 1) {
604
+ continue
605
+ }
606
+ const event: i32 = this.h3.next()
607
+ if (event === H3_NEED_MORE || event === H3_ERROR) {
608
+ return event
609
+ }
610
+ const routed: i32 = this.route(event)
611
+ if (routed !== H3_NEED_MORE) {
612
+ return routed
613
+ }
614
+ if (this.generation !== this.h3.generation) {
615
+ return H3_NEED_MORE
616
+ }
617
+ }
618
+ return H3_NEED_MORE
619
+ }
620
+
621
+ /**
622
+ * Reads one QUIC datagram, if one is waiting: answers WT_DATAGRAM for one
623
+ * of an accepted session, 1 for one dropped, or 0 when none is there.
624
+ */
625
+ receiveDatagram(): i32 {
626
+ const n: i32 = this.quic.readDatagram(this.datagram, WT_ZERO, toI32(this.datagram.length))
627
+ if (n === QUIC_DATAGRAM_NONE || n < 0) {
628
+ return WT_ZERO
629
+ }
630
+ const quarter: i64 = h3ReadVarint(this.datagram, WT_ZERO, n)
631
+ if (quarter < 0 || quarter > WT_QUARTER_MAX) {
632
+ // RFC 9297 §2.1: a quarter stream ID that does not parse, or is too large.
633
+ this.h3.fail(H3_DATAGRAM_ERROR)
634
+ return WT_ONE
635
+ }
636
+ const s: i32 = this.sessionOf(quarter * 4)
637
+ if (s < 0 || this.states[s] !== WT_OPEN) {
638
+ // §2.1.1: no session to take it; dropped.
639
+ this.datagramsDropped = h3Count(this.datagramsDropped)
640
+ return WT_ONE
641
+ }
642
+ const at: i32 = h3VarintLength(this.datagram, WT_ZERO)
643
+ this.datagramsIn[s] = h3Count(this.datagramsIn[s])
644
+ this.session = s
645
+ this.sessionId = this.sessionIds[s]
646
+ this.data = this.datagram
647
+ this.dataStart = at
648
+ this.dataLength = n - at
649
+ return WT_DATAGRAM
650
+ }
651
+
652
+ /** What an `Http3Connection` event means here: a `WT_*` event, the same event passed through, or H3_NEED_MORE. */
653
+ route(event: i32): i32 {
654
+ const h3: Http3Connection = this.h3
655
+ const id: i64 = h3.stream
656
+ if (event === H3_STREAM) {
657
+ return this.arrived(id, h3.session)
658
+ }
659
+ if (event === H3_GOAWAY) {
660
+ return event
661
+ }
662
+ if (event === H3_DROPPED) {
663
+ // A request that never reached the program: no session will come of it.
664
+ this.dropPending(id)
665
+ return H3_NEED_MORE
666
+ }
667
+ if (event === H3_REQUEST) {
668
+ if (toI32(h3.fields.protocol.length) > 0) {
669
+ return this.asked(id)
670
+ }
671
+ // A plain request: no session, so nothing waiting for one on its stream.
672
+ this.dropPending(id)
673
+ return event
674
+ }
675
+ // The slot the event's stream was in: QUIC may have freed it with the read that ended it.
676
+ const k: i32 = h3.slot >= 0 && h3.slot < toI32(this.streamIds.length) ? h3.slot : WT_NONE
677
+ const s: i32 = k >= 0 ? this.sessionAt[k] : WT_NONE
678
+ if (s >= 0 && s < toI32(this.sessionIds.length) && this.sessionIds[s] === id) {
679
+ return this.connectEvent(s, event)
680
+ }
681
+ if (k < 0 || this.streamIds[k] !== id) {
682
+ // A plain request's, or one about a stream this layer refused or let go of.
683
+ return event === H3_STOPPED ? H3_NEED_MORE : event
684
+ }
685
+ const t: i32 = this.streamSession[k]
686
+ this.stream = id
687
+ this.session = t
688
+ this.sessionId = t >= 0 && t < toI32(this.sessionIds.length) ? this.sessionIds[t] : WT_NONE64
689
+ if (event === H3_DATA) {
690
+ this.data = h3.data
691
+ this.dataStart = h3.dataStart
692
+ this.dataLength = h3.dataLength
693
+ return WT_STREAM_DATA
694
+ }
695
+ if (event === H3_END) {
696
+ this.settle(k, WT_RECEIVED)
697
+ return WT_STREAM_END
698
+ }
699
+ if (event === H3_RESET) {
700
+ this.codes(h3.errorCode)
701
+ this.settle(k, WT_RECEIVED)
702
+ return WT_STREAM_RESET
703
+ }
704
+ if (event === H3_STOPPED) {
705
+ this.codes(h3.errorCode)
706
+ this.settle(k, WT_SENT)
707
+ return WT_STREAM_STOPPED
708
+ }
709
+ return event === H3_WRITABLE ? WT_WRITABLE : H3_NEED_MORE
710
+ }
711
+
712
+ /** Sets `errorCode` to `code` and `appCode` to what it maps to. */
713
+ codes(code: i64): void {
714
+ this.errorCode = code
715
+ this.appCode = wtCodeFromHttp3(code)
716
+ }
717
+
718
+ /** An event of session `s`'s CONNECT stream. */
719
+ connectEvent(s: i32, event: i32): i32 {
720
+ const h3: Http3Connection = this.h3
721
+ this.session = s
722
+ this.sessionId = this.sessionIds[s]
723
+ this.stream = this.sessionIds[s]
724
+ if (event === H3_DATA) {
725
+ if (this.states[s] === WT_GONE && !this.closeRead[s]) {
726
+ // This side closed it: what the client still sends is not read.
727
+ return H3_NEED_MORE
728
+ }
729
+ this.capSession = s
730
+ this.capAt = h3.dataStart
731
+ this.capEnd = h3.dataStart + h3.dataLength
732
+ return this.capsules()
733
+ }
734
+ if (event === H3_END || event === H3_RESET) {
735
+ // The client's half is over, and the session with it (draft-02 §5).
736
+ const id: i64 = this.sessionIds[s]
737
+ const wasGone: boolean = this.states[s] === WT_GONE
738
+ const inside: boolean = this.capLeft[s] >= 0 || this.capHeadLen[s] > 0
739
+ this.release(s)
740
+ if (wasGone) {
741
+ // This side's half is finished already.
742
+ return H3_NEED_MORE
743
+ }
744
+ if (event === H3_RESET) {
745
+ // `nish/net/http3` abandoned this side's half with the reset, the client's or its own.
746
+ return this.closedEvent(s, WT_ZERO64, WT_ZERO, h3.resetByPeer)
747
+ }
748
+ if (inside) {
749
+ // RFC 9297 §3.3: the stream ended inside a capsule.
750
+ h3.reset(id, H3_DATAGRAM_ERROR)
751
+ return this.closedEvent(s, WT_ZERO64, WT_ZERO, false)
752
+ }
753
+ this.finish(id)
754
+ return this.closedEvent(s, WT_ZERO64, WT_ZERO, true)
755
+ }
756
+ // Trailers on a CONNECT stream mean nothing to a session.
757
+ return event === H3_WRITABLE ? WT_WRITABLE : H3_NEED_MORE
758
+ }
759
+
760
+ /**
761
+ * Finishes this side's half of CONNECT stream `id` with its FIN, or with
762
+ * a reset both ways when the FIN cannot go — a session never answered has
763
+ * no head for it to follow. Answers whether the FIN went: after a reset
764
+ * the stream is dropped unread, so no event will end the session later.
765
+ */
766
+ finish(id: i64): boolean {
767
+ if (this.h3.writeData(id, this.reason, WT_ZERO, WT_ZERO, true) >= 0) {
768
+ return true
769
+ }
770
+ this.h3.reset(id, H3_NO_ERROR)
771
+ return false
772
+ }
773
+
774
+ /** WT_CLOSED for session `s` with `code` and the reason `reason[s * 1028 + 4 ..][0 .. length)`. */
775
+ closedEvent(s: i32, code: i64, length: i32, byPeer: boolean): i32 {
776
+ this.session = s
777
+ this.errorCode = code & WT_U32
778
+ this.appCode = this.errorCode
779
+ this.closedByPeer = byPeer
780
+ this.data = this.reason
781
+ this.dataStart = s * WT_REASON_STRIDE + 4
782
+ this.dataLength = length
783
+ return WT_CLOSED
784
+ }
785
+
786
+ // ---- Sessions --------------------------------------------------------------------
787
+
788
+ /**
789
+ * An extended CONNECT arrived on `id`: a session when it is one this side
790
+ * takes, answered here when not.
791
+ */
792
+ asked(id: i64): i32 {
793
+ const h3: Http3Connection = this.h3
794
+ const f: HttpFields = h3.fields
795
+ const k: i32 = this.quic.streams.slotOf(id)
796
+ if (k >= 0 && k < toI32(this.sessionAt.length)) {
797
+ const stale: i32 = this.sessionAt[k]
798
+ if (stale >= 0 && stale < toI32(this.sessionIds.length) && this.sessionIds[stale] >= 0) {
799
+ // QUIC freed this slot under a session's CONNECT stream with no event to say so: let go of
800
+ // that session now, before the cap below counts it.
801
+ this.release(stale)
802
+ }
803
+ }
804
+ let status: i32 = 0
805
+ const peer = h3.peer
806
+ if (!httpFieldIs(f.protocol, "webtransport")) {
807
+ status = 501
808
+ } else if (
809
+ !httpFieldIs(f.scheme, "https") ||
810
+ peer.h3Datagram !== toI64(1) ||
811
+ (peer.enableWebtransport !== toI64(1) && peer.webtransportMaxSessions <= 0)
812
+ ) {
813
+ status = 400
814
+ } else if (this.freeCount <= 0) {
815
+ status = 429
816
+ }
817
+ if (status !== 0) {
818
+ this.answer(id, status)
819
+ this.dropPending(id)
820
+ return H3_NEED_MORE
821
+ }
822
+ if (k < 0 || k >= toI32(this.sessionAt.length)) {
823
+ return H3_NEED_MORE
824
+ }
825
+ this.freeCount = this.freeCount - 1
826
+ const s: i32 = this.free[this.freeCount]
827
+ this.sessionIds[s] = id
828
+ this.states[s] = WT_ASKED
829
+ this.sessionSlot[s] = k
830
+ this.firstStream[s] = -1
831
+ this.streamCount[s] = 0
832
+ this.capType[s] = -1
833
+ this.capLeft[s] = -1
834
+ this.closeRead[s] = false
835
+ this.capHeadLen[s] = 0
836
+ this.capFill[s] = 0
837
+ this.datagramsIn[s] = 0
838
+ this.datagramsOut[s] = 0
839
+ this.sessionAt[k] = s
840
+ this.session = s
841
+ this.sessionId = id
842
+ this.stream = id
843
+ return WT_SESSION
844
+ }
845
+
846
+ /** Answers the request on `id` with `status` and stops reading it: a session refused. */
847
+ answer(id: i64, status: i32): void {
848
+ this.sessionsRefused = h3Count(this.sessionsRefused)
849
+ if (this.h3.respond(id, status, this.none, this.none, true) !== 0) {
850
+ this.h3.reset(id, H3_REQUEST_REJECTED)
851
+ }
852
+ this.h3.discardRequest(id, H3_NO_ERROR)
853
+ }
854
+
855
+ /**
856
+ * Accepts session `id` with a 200 (draft-02 §3.2): its datagrams and
857
+ * streams reach the program from now on, the streams that waited for it
858
+ * first. Answers 0, H3_AGAIN when the response does not fit yet, or
859
+ * H3_CLOSED for one that is not a session asked for.
860
+ */
861
+ accept(id: i64): i32 {
862
+ this.current()
863
+ const s: i32 = this.sessionOf(id)
864
+ if (s < 0 || this.states[s] !== WT_ASKED) {
865
+ return H3_CLOSED
866
+ }
867
+ const result: i32 = this.h3.respond(id, WT_STATUS_OK, this.acceptNames, this.acceptValues, false)
868
+ if (result !== 0) {
869
+ return result
870
+ }
871
+ this.states[s] = WT_OPEN
872
+ // The streams that waited for it are taken by the next calls of `next`, each with its WT_STREAM.
873
+ if (this.waiting.first(id) >= 0 && this.adoptCount < toI32(this.adoptions.length)) {
874
+ this.adoptions[this.adoptCount] = s
875
+ this.adoptCount = this.adoptCount + 1
876
+ }
877
+ return 0
878
+ }
879
+
880
+ /**
881
+ * Refuses session `id` with `status`, 400 to 599 (draft-02 §3.2): the
882
+ * response ends the stream, and what the client still sends is dropped.
883
+ * Answers 0, H3_INVALID for a status out of range, or H3_CLOSED.
884
+ */
885
+ refuse(id: i64, status: i32): i32 {
886
+ this.current()
887
+ const s: i32 = this.sessionOf(id)
888
+ if (s < 0 || this.states[s] !== WT_ASKED) {
889
+ return H3_CLOSED
890
+ }
891
+ if (status < 400 || status > 599) {
892
+ return H3_INVALID
893
+ }
894
+ this.answer(id, status)
895
+ this.release(s)
896
+ return 0
897
+ }
898
+
899
+ /**
900
+ * Ends session `s` here: every stream reset with WT_SESSION_GONE and asked
901
+ * to stop, the streams waiting for it refused, and its slot freed.
902
+ */
903
+ release(s: i32): void {
904
+ this.end(s)
905
+ const k: i32 = this.sessionSlot[s]
906
+ if (k >= 0 && k < toI32(this.sessionAt.length) && this.sessionAt[k] === s) {
907
+ this.sessionAt[k] = -1
908
+ }
909
+ this.sessionIds[s] = -1
910
+ this.states[s] = WT_FREE
911
+ if (this.capSession === s) {
912
+ this.capSession = -1
913
+ }
914
+ if (this.freeCount < toI32(this.free.length)) {
915
+ this.free[this.freeCount] = s
916
+ this.freeCount = this.freeCount + 1
917
+ }
918
+ }
919
+
920
+ /** Resets every stream of session `s` and refuses those waiting for it; the session is gone. */
921
+ end(s: i32): void {
922
+ if (this.states[s] === WT_FREE) {
923
+ return
924
+ }
925
+ let k: i32 = this.firstStream[s]
926
+ for (let guard: i32 = 0; guard < WT_MAX_CAP && k >= 0 && k < toI32(this.streamIds.length); guard++) {
927
+ const following: i32 = this.nextStream[k]
928
+ this.h3.refuseStream(this.streamIds[k], WT_SESSION_GONE)
929
+ this.streamIds[k] = -1
930
+ this.streamSession[k] = -1
931
+ k = following
932
+ }
933
+ this.firstStream[s] = -1
934
+ this.streamCount[s] = 0
935
+ this.dropPending(this.sessionIds[s])
936
+ this.states[s] = WT_GONE
937
+ }
938
+
939
+ /** Refuses with WT_SESSION_GONE every stream waiting for session `id`. */
940
+ dropPending(id: i64): void {
941
+ if (this.waiting.count === 0) {
942
+ return
943
+ }
944
+ let e: i32 = this.waiting.first(id)
945
+ for (let guard: i32 = 0; guard < WT_MAX_CAP && e >= 0; guard++) {
946
+ this.refuseStream(this.waiting.take(e), WT_SESSION_GONE)
947
+ e = this.waiting.first(id)
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Closes session `id` (draft-02 §5): CLOSE_WEBTRANSPORT_SESSION with the
953
+ * application's `code` and the reason `buf[off .. off + len)`, at most
954
+ * 1024 bytes of UTF-8, then the FIN; every stream of the session is reset
955
+ * with WT_SESSION_GONE. Answers 0; H3_INVALID for a reason too long;
956
+ * H3_AGAIN while the CONNECT stream has no room for the capsule, and
957
+ * WT_WRITABLE then names it; or H3_CLOSED.
958
+ */
959
+ close(id: i64, code: i64, buf: u8[], off: i32, len: i32): i32 {
960
+ h3CheckWindow("WebTransport.close", buf, off, len)
961
+ const s: i32 = this.openSession(id)
962
+ if (s < 0) {
963
+ return H3_CLOSED
964
+ }
965
+ if (len > WT_REASON_MAX) {
966
+ return H3_INVALID
967
+ }
968
+ const out: u8[] = this.capsuleOut
969
+ const end: i32 = toI32(out.length)
970
+ let p: i32 = h3PutFrameHeader(out, WT_ZERO, end, WT_CAPSULE_CLOSE, toI64(4 + len))
971
+ const value: i64 = code & WT_U32
972
+ out[p] = toU8(toI32((value >> toI64(24)) & toI64(255)))
973
+ out[p + 1] = toU8(toI32((value >> toI64(16)) & toI64(255)))
974
+ out[p + 2] = toU8(toI32((value >> toI64(8)) & toI64(255)))
975
+ out[p + 3] = toU8(toI32(value & toI64(255)))
976
+ p = p + 4
977
+ quicPacketCopy(out, p, buf, off, len)
978
+ const result: i32 = this.h3.writeDataWhole(id, out, WT_ZERO, p + len, true)
979
+ if (result !== 0) {
980
+ return result
981
+ }
982
+ this.end(s)
983
+ return 0
984
+ }
985
+
986
+ /**
987
+ * Sends DRAIN_WEBTRANSPORT_SESSION on session `id`: the client should
988
+ * finish and close it. Answers 0, H3_AGAIN, or H3_CLOSED.
989
+ */
990
+ drain(id: i64): i32 {
991
+ const s: i32 = this.openSession(id)
992
+ if (s < 0) {
993
+ return H3_CLOSED
994
+ }
995
+ const out: u8[] = this.capsuleOut
996
+ const end: i32 = toI32(out.length)
997
+ const p: i32 = h3PutFrameHeader(out, WT_ZERO, end, WT_CAPSULE_DRAIN, toI64(0))
998
+ return this.h3.writeDataWhole(id, out, WT_ZERO, p, false)
999
+ }
1000
+
1001
+ // ---- Capsules --------------------------------------------------------------------
1002
+
1003
+ /**
1004
+ * Reads the capsules of the DATA window `h3.data[capAt .. capEnd)` of
1005
+ * session `capSession`'s CONNECT stream, and answers the first event they
1006
+ * make, or H3_NEED_MORE once the window is read.
1007
+ */
1008
+ capsules(): i32 {
1009
+ const s: i32 = this.capSession
1010
+ const bytes: u8[] = this.h3.data
1011
+ const base: i32 = s * WT_HEAD
1012
+ while (this.capAt < this.capEnd && this.capAt >= 0 && this.capAt < toI32(bytes.length)) {
1013
+ if (this.closeRead[s]) {
1014
+ // §5: nothing may follow a CLOSE.
1015
+ return this.malformed(s, H3_MESSAGE_ERROR)
1016
+ }
1017
+ if (this.capLeft[s] < 0) {
1018
+ // The header, a byte at a time into the session's own buffer: it may be split across DATA frames.
1019
+ const have: i32 = this.capHeadLen[s]
1020
+ if (have >= WT_HEAD || base + have >= toI32(this.capHead.length)) {
1021
+ return this.malformed(s, H3_DATAGRAM_ERROR)
1022
+ }
1023
+ this.capHead[base + have] = bytes[this.capAt]
1024
+ this.capAt = this.capAt + 1
1025
+ this.capHeadLen[s] = have + 1
1026
+ if (h3ReadFrameHeader(this.header, this.capHead, base, have + 1) === 0) {
1027
+ continue
1028
+ }
1029
+ this.capHeadLen[s] = 0
1030
+ const type: i64 = this.header.type
1031
+ const length: i64 = this.header.length
1032
+ if (type === WT_CAPSULE_CLOSE && (length < 4 || length > toI64(WT_REASON_MAX + 4))) {
1033
+ return this.malformed(s, H3_DATAGRAM_ERROR)
1034
+ }
1035
+ if (type === WT_CAPSULE_DRAIN && length !== toI64(0)) {
1036
+ return this.malformed(s, H3_DATAGRAM_ERROR)
1037
+ }
1038
+ this.capType[s] = type
1039
+ this.capLeft[s] = length
1040
+ this.capFill[s] = 0
1041
+ } else {
1042
+ const want: i64 = toI64(this.capEnd - this.capAt)
1043
+ const take: i32 = toI32(this.capLeft[s] < want ? this.capLeft[s] : want)
1044
+ if (this.capType[s] === WT_CAPSULE_CLOSE) {
1045
+ quicPacketCopy(this.reason, s * WT_REASON_STRIDE + this.capFill[s], bytes, this.capAt, take)
1046
+ this.capFill[s] = this.capFill[s] + take
1047
+ }
1048
+ this.capAt = this.capAt + take
1049
+ this.capLeft[s] = this.capLeft[s] - toI64(take)
1050
+ }
1051
+ if (this.capLeft[s] === toI64(0)) {
1052
+ this.capLeft[s] = -1
1053
+ const event: i32 = this.capsule(s, this.capType[s])
1054
+ if (event !== H3_NEED_MORE) {
1055
+ return event
1056
+ }
1057
+ }
1058
+ }
1059
+ this.capSession = -1
1060
+ return H3_NEED_MORE
1061
+ }
1062
+
1063
+ /** A whole capsule of `type` on session `s`'s CONNECT stream. */
1064
+ capsule(s: i32, type: i64): i32 {
1065
+ this.session = s
1066
+ this.sessionId = this.sessionIds[s]
1067
+ this.stream = this.sessionIds[s]
1068
+ if (type === WT_CAPSULE_DRAIN) {
1069
+ return this.states[s] === WT_GONE ? H3_NEED_MORE : WT_DRAIN
1070
+ }
1071
+ if (type !== WT_CAPSULE_CLOSE) {
1072
+ // RFC 9297 §3.2: a capsule type this side does not know is skipped.
1073
+ return H3_NEED_MORE
1074
+ }
1075
+ this.closeRead[s] = true
1076
+ if (this.states[s] === WT_GONE) {
1077
+ // This side closed it first, and finished its half then.
1078
+ return H3_NEED_MORE
1079
+ }
1080
+ const at: i32 = s * WT_REASON_STRIDE
1081
+ let code: i64 = 0
1082
+ for (let j: i32 = 0; j < 4 && at + j < toI32(this.reason.length); j++) {
1083
+ code = (code << toI64(8)) | toI64(toI32(this.reason[at + j]))
1084
+ }
1085
+ this.end(s)
1086
+ const length: i32 = this.capFill[s] - 4
1087
+ // §5: the CLOSE is answered by finishing this side's half; when that cannot go, the session is over here and now.
1088
+ if (!this.finish(this.sessionIds[s])) {
1089
+ this.release(s)
1090
+ }
1091
+ return this.closedEvent(s, code, length, true)
1092
+ }
1093
+
1094
+ /** A capsule that breaks a rule on session `s`: its CONNECT stream reset both ways with `code`, and the session over. */
1095
+ malformed(s: i32, code: i64): i32 {
1096
+ const id: i64 = this.sessionIds[s]
1097
+ const wasGone: boolean = this.states[s] === WT_GONE
1098
+ this.capSession = -1
1099
+ this.release(s)
1100
+ this.h3.reset(id, code)
1101
+ this.h3.discardRequest(id, code)
1102
+ this.stream = id
1103
+ this.sessionId = id
1104
+ return wasGone ? H3_NEED_MORE : this.closedEvent(s, WT_ZERO64, WT_ZERO, false)
1105
+ }
1106
+
1107
+ // ---- Streams ---------------------------------------------------------------------
1108
+
1109
+ /** A stream named session `session` in its first bytes (H3_STREAM): taken, kept waiting or refused. */
1110
+ arrived(id: i64, session: i64): i32 {
1111
+ const s: i32 = this.sessionOf(session)
1112
+ if (s >= 0 && this.states[s] === WT_OPEN) {
1113
+ return this.adopt(s, id)
1114
+ }
1115
+ if ((s >= 0 && this.states[s] === WT_ASKED) || (s < 0 && this.h3.awaitsRequest(session))) {
1116
+ // draft-02 §4.5: it waits, unread, for its session.
1117
+ if (this.waiting.count < this.config.maxPending && this.waiting.add(id, session)) {
1118
+ return H3_NEED_MORE
1119
+ }
1120
+ this.refuseStream(id, WT_BUFFERED_STREAM_REJECTED)
1121
+ return H3_NEED_MORE
1122
+ }
1123
+ this.refuseStream(id, WT_SESSION_GONE)
1124
+ return H3_NEED_MORE
1125
+ }
1126
+
1127
+ /**
1128
+ * Takes the oldest stream still waiting for the last session accepted
1129
+ * with streams waiting: answers its WT_STREAM, or H3_NEED_MORE once that
1130
+ * session has none left (or is gone), when the next session's turn comes.
1131
+ */
1132
+ adoptWaiting(): i32 {
1133
+ const s: i32 = this.adoptions[this.adoptCount - 1]
1134
+ if (s >= 0 && s < toI32(this.states.length) && this.states[s] === WT_OPEN) {
1135
+ const e: i32 = this.waiting.first(this.sessionIds[s])
1136
+ if (e >= 0) {
1137
+ return this.adopt(s, this.waiting.take(e))
1138
+ }
1139
+ }
1140
+ this.adoptCount = this.adoptCount - 1
1141
+ return H3_NEED_MORE
1142
+ }
1143
+
1144
+ /** Refuses stream `id` with `code`, counted. */
1145
+ refuseStream(id: i64, code: i64): void {
1146
+ this.streamsRefused = h3Count(this.streamsRefused)
1147
+ this.h3.refuseStream(id, code)
1148
+ }
1149
+
1150
+ /**
1151
+ * Takes the client's stream `id` into session `s`, unless the session
1152
+ * holds `maxStreams` already: answers WT_STREAM, or H3_NEED_MORE for one
1153
+ * refused with H3_REQUEST_REJECTED.
1154
+ */
1155
+ adopt(s: i32, id: i64): i32 {
1156
+ if (this.streamCount[s] >= this.config.maxStreams) {
1157
+ this.refuseStream(id, H3_REQUEST_REJECTED)
1158
+ return H3_NEED_MORE
1159
+ }
1160
+ const bidi: boolean = !quicStreamIsUni(id)
1161
+ if (this.h3.acceptStream(id) !== 0 || this.link(s, id, bidi ? WT_ZERO : WT_SENT) < 0) {
1162
+ return H3_NEED_MORE
1163
+ }
1164
+ this.session = s
1165
+ this.sessionId = this.sessionIds[s]
1166
+ this.stream = id
1167
+ this.bidirectional = bidi
1168
+ return WT_STREAM
1169
+ }
1170
+
1171
+ /** Puts stream `id` at the head of session `s`'s list with `bits`; answers its slot, or -1. */
1172
+ link(s: i32, id: i64, bits: i32): i32 {
1173
+ const k: i32 = this.quic.streams.slotOf(id)
1174
+ if (k < 0 || k >= toI32(this.streamIds.length)) {
1175
+ return WT_NONE
1176
+ }
1177
+ if (this.streamIds[k] >= 0) {
1178
+ // QUIC freed this slot under a stream whose end no event told (a STOP_SENDING it answered and saw acknowledged): let go of it now.
1179
+ this.unlink(k)
1180
+ }
1181
+ const head: i32 = this.firstStream[s]
1182
+ this.streamIds[k] = id
1183
+ this.streamSession[k] = s
1184
+ this.streamBits[k] = bits
1185
+ this.prevStream[k] = -1
1186
+ this.nextStream[k] = head
1187
+ if (head >= 0 && head < toI32(this.prevStream.length)) {
1188
+ this.prevStream[head] = k
1189
+ }
1190
+ this.firstStream[s] = k
1191
+ this.streamCount[s] = this.streamCount[s] + 1
1192
+ return k
1193
+ }
1194
+
1195
+ /** Marks `bits` done on the stream in slot `k`, and takes it off its session once both sides are. */
1196
+ settle(k: i32, bits: i32): void {
1197
+ this.streamBits[k] = this.streamBits[k] | bits
1198
+ if ((this.streamBits[k] & (WT_SENT | WT_RECEIVED)) === (WT_SENT | WT_RECEIVED)) {
1199
+ this.unlink(k)
1200
+ }
1201
+ }
1202
+
1203
+ /** Takes the stream in slot `k` off its session's list. */
1204
+ unlink(k: i32): void {
1205
+ const s: i32 = this.streamSession[k]
1206
+ const before: i32 = this.prevStream[k]
1207
+ const after: i32 = this.nextStream[k]
1208
+ if (before >= 0 && before < toI32(this.nextStream.length)) {
1209
+ this.nextStream[before] = after
1210
+ } else if (s >= 0 && s < toI32(this.firstStream.length)) {
1211
+ this.firstStream[s] = after
1212
+ }
1213
+ if (after >= 0 && after < toI32(this.prevStream.length)) {
1214
+ this.prevStream[after] = before
1215
+ }
1216
+ if (s >= 0 && s < toI32(this.streamCount.length)) {
1217
+ this.streamCount[s] = this.streamCount[s] - 1
1218
+ }
1219
+ this.streamIds[k] = -1
1220
+ this.streamSession[k] = -1
1221
+ }
1222
+
1223
+ /**
1224
+ * Opens a stream of this side's for session `id` (draft-02 §4.1, §4.2).
1225
+ * Answers its ID; WT_LIMIT when the session holds `maxStreams`; H3_AGAIN
1226
+ * while the client's MAX_STREAMS allows no more; or H3_CLOSED.
1227
+ */
1228
+ openStream(id: i64, bidirectional: boolean): i64 {
1229
+ const s: i32 = this.openSession(id)
1230
+ if (s < 0) {
1231
+ return toI64(H3_CLOSED)
1232
+ }
1233
+ if (this.streamCount[s] >= this.config.maxStreams) {
1234
+ return toI64(WT_LIMIT)
1235
+ }
1236
+ const stream: i64 = this.h3.openStream(id, bidirectional)
1237
+ if (stream < 0) {
1238
+ return stream
1239
+ }
1240
+ this.link(s, stream, bidirectional ? WT_ZERO : WT_RECEIVED)
1241
+ return stream
1242
+ }
1243
+
1244
+ /**
1245
+ * Writes up to `len` bytes of `buf` from `off` to session stream `id`, with
1246
+ * its FIN when `fin` and all were taken. Answers how many it took, or
1247
+ * H3_AGAIN, or H3_CLOSED (see `Http3Connection.writeStream`).
1248
+ */
1249
+ write(id: i64, buf: u8[], off: i32, len: i32, fin: boolean): i32 {
1250
+ this.current()
1251
+ const k: i32 = this.streamSlot(id)
1252
+ if (k < 0) {
1253
+ return H3_CLOSED
1254
+ }
1255
+ const n: i32 = this.h3.writeStream(id, buf, off, len, fin)
1256
+ if (fin && n === len) {
1257
+ this.settle(k, WT_SENT)
1258
+ }
1259
+ return n
1260
+ }
1261
+
1262
+ /** Resets this side of session stream `id` with the application's `code` (mapped, `wtCodeToHttp3`). Answers 0 or H3_CLOSED. */
1263
+ resetStream(id: i64, code: i64): i32 {
1264
+ this.current()
1265
+ const k: i32 = this.streamSlot(id)
1266
+ if (k < 0) {
1267
+ return H3_CLOSED
1268
+ }
1269
+ const result: i32 = this.h3.resetStream(id, wtCodeToHttp3(code))
1270
+ if (result !== 0) {
1271
+ return result
1272
+ }
1273
+ this.settle(k, WT_SENT)
1274
+ return 0
1275
+ }
1276
+
1277
+ /** Asks the client to stop its side of session stream `id` with the application's `code`, and drops what it sends. Answers 0 or H3_CLOSED. */
1278
+ stopSending(id: i64, code: i64): i32 {
1279
+ this.current()
1280
+ const k: i32 = this.streamSlot(id)
1281
+ if (k < 0 || (this.streamBits[k] & WT_RECEIVED) !== 0) {
1282
+ return H3_CLOSED
1283
+ }
1284
+ const result: i32 = this.h3.stopStream(id, wtCodeToHttp3(code))
1285
+ if (result !== 0) {
1286
+ return result
1287
+ }
1288
+ this.settle(k, WT_RECEIVED)
1289
+ return 0
1290
+ }
1291
+
1292
+ // ---- Datagrams -------------------------------------------------------------------
1293
+
1294
+ /**
1295
+ * The largest payload `sendDatagram` takes for session `id` now: what the
1296
+ * QUIC connection's `maxDatagramPayload` leaves after the quarter stream
1297
+ * ID; 0 for a session not accepted.
1298
+ */
1299
+ maxDatagramPayload(id: i64): i32 {
1300
+ if (this.openSession(id) < 0) {
1301
+ return WT_ZERO
1302
+ }
1303
+ const room: i32 = this.quic.maxDatagramPayload() - quicVarintSize(id >> toI64(2))
1304
+ return room > 0 ? room : WT_ZERO
1305
+ }
1306
+
1307
+ /**
1308
+ * Sends `buf[off .. off + len)` as an HTTP datagram of session `id` (RFC
1309
+ * 9297 §2.1): its quarter stream ID, then the payload, one QUIC DATAGRAM
1310
+ * frame, never sent again if lost. Answers 0; H3_TOO_LARGE past
1311
+ * `maxDatagramPayload`; H3_AGAIN while QUIC's queue is full; or H3_CLOSED
1312
+ * for a session not accepted.
1313
+ */
1314
+ sendDatagram(id: i64, buf: u8[], off: i32, len: i32): i32 {
1315
+ h3CheckWindow("WebTransport.sendDatagram", buf, off, len)
1316
+ const s: i32 = this.openSession(id)
1317
+ if (s < 0) {
1318
+ return H3_CLOSED
1319
+ }
1320
+ const out: u8[] = this.outgoing
1321
+ const p: i32 = h3PutVarint(out, WT_ZERO, toI32(out.length), id >> toI64(2))
1322
+ // The quarter stream ID and the payload must fit what QUIC sends now (`maxDatagramPayload`, without the ID's room).
1323
+ if (p < 0 || len > this.quic.maxDatagramPayload() - p || p + len > toI32(out.length)) {
1324
+ return H3_TOO_LARGE
1325
+ }
1326
+ quicPacketCopy(out, p, buf, off, len)
1327
+ const result: i32 = this.quic.sendDatagram(out, WT_ZERO, p + len)
1328
+ if (result === QUIC_DATAGRAM_OK) {
1329
+ this.datagramsOut[s] = h3Count(this.datagramsOut[s])
1330
+ return 0
1331
+ }
1332
+ return result === QUIC_DATAGRAM_ERR_FULL ? H3_AGAIN : H3_TOO_LARGE
1333
+ }
1334
+ }