@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,911 @@
1
+ /**
2
+ * `nish/net/http3-server` — HTTP/3 over QUIC on one UDP socket: a pool of
3
+ * slots, each a reusable `QuicConnection` with an `Http3Connection` beside
4
+ * it, a `QuicListener` in front for the datagrams no slot owns, for a
5
+ * program that owns its readiness loop.
6
+ *
7
+ * const fd = udpBind("0.0.0.0", 4433, 0);
8
+ * const server = new Http3Server(quicConfig, new Http3Config(), fd, 64, listenerEntropy);
9
+ * // quicConfig.alpn is ["h3"]; pollAdd(loop, fd, 1, 0)
10
+ * // each time round the loop, with `now` in milliseconds:
11
+ * pollWait(loop, ready, server.timeout(now));
12
+ * server.receive(now, key); // every datagram waiting; signs handshakes with `key`
13
+ * server.tick(now); // the connections' timers
14
+ * let slot: i32 = server.ready();
15
+ * while (slot >= 0) {
16
+ * const h3: Http3Connection = server.connection(slot);
17
+ * let event: i32 = h3.next();
18
+ * while (event !== H3_NEED_MORE && event !== H3_ERROR) { … ; event = h3.next(); }
19
+ * slot = server.ready();
20
+ * }
21
+ * server.flush(now); // what every touched slot has to send
22
+ *
23
+ * **Routing.** A datagram goes to the slot that owns its first packet's
24
+ * Destination Connection ID, found through a hash index of every slot's
25
+ * connection IDs — its active local IDs, and the client's original DCID until
26
+ * the handshake is done — never by asking each slot. The index is keyed with
27
+ * bytes from the listener's entropy, so a client choosing its original DCIDs
28
+ * cannot pick ones that collide. A datagram no slot owns goes to the listener,
29
+ * which answers Version Negotiation, Retry or a stateless reset, or accepts a
30
+ * new connection into a free slot. With no slot free, the datagram is dropped
31
+ * and counted, and the client's own timers retry.
32
+ *
33
+ * **ALPN.** The QUIC configuration's ALPN list is what the TLS handshake
34
+ * offers; a connection whose handshake chose anything but `h3` is closed by
35
+ * its `Http3Connection`, before a byte of HTTP/3, with CRYPTO_ERROR
36
+ * no_application_protocol (0x0178; RFC 9114 §3.1).
37
+ *
38
+ * **Sending.** `flush` writes each touched slot's datagrams with
39
+ * `quicListenerTakeFlight`, a paced flight at a time in one `udpSendTo` with
40
+ * GSO where the platform has it and one datagram at a time where it does not.
41
+ * A slot whose HTTP/3 side sent GOAWAY and whose requests have all finished,
42
+ * every byte acknowledged, is closed with H3_NO_ERROR; a closed slot is freed
43
+ * once its CONNECTION_CLOSE is out.
44
+ *
45
+ * **Timers** are a wheel of `H3_SERVER_WHEEL` one-millisecond buckets, a
46
+ * slot in at most one: after every datagram and every flush the slot is filed
47
+ * under its next deadline (its idle, loss and probe timers, and its pacer),
48
+ * and `tick` runs the buckets that are due. `timeout` finds the next filled
49
+ * bucket from a bitmap. A deadline past the wheel's horizon is filed at the
50
+ * horizon and filed again when it comes round. So no call scans the slots.
51
+ *
52
+ * **Caps, fixed at start-up**: the slots, and inside each the QUIC
53
+ * configuration's streams and buffers and the `Http3Config`'s field section,
54
+ * body chunk and DATA frame. The constructor makes every slot's connections;
55
+ * a datagram an established connection reads, and every datagram it sends,
56
+ * allocates nothing; nor does a datagram no slot owns, which the listener
57
+ * reads where it lies and answers into scratch made once, nor a handshake,
58
+ * whose state is the slot's — `docs/security/http3.md` (H3-1, H3-3).
59
+ *
60
+ * Written from RFC 9114 §3 and RFC 9000 §5.2, not ported from another
61
+ * implementation. Private names carry the `http3Server` prefix
62
+ * (`docs/wp26-stdlib.md` §3e).
63
+ */
64
+ import { udpRecvFrom, udpSendTo } from "nish:net"
65
+ import { Secret } from "nish:secret"
66
+ import { Http3Config, Http3Connection } from "nish/net/http3"
67
+ import { H3_NO_ERROR, h3Count } from "nish/net/http3-frame"
68
+ import {
69
+ QUIC_CONN_CID_LENGTH,
70
+ QUIC_CONN_DATAGRAM_SIZE,
71
+ QUIC_CONN_ENTROPY_SIZE,
72
+ QUIC_CONN_LOCAL_CIDS,
73
+ QuicConnection,
74
+ QuicServerConfig,
75
+ } from "nish/net/quic"
76
+ import { QUIC_MAX_CID_LENGTH } from "nish/net/quic-packet"
77
+ import {
78
+ QUIC_LISTEN_ACCEPT,
79
+ QUIC_LISTENER_ANSWER_SIZE,
80
+ QUIC_LISTENER_FLIGHT_MAX,
81
+ QUIC_LISTENER_MIN_RESET,
82
+ QuicFlight,
83
+ QuicListener,
84
+ QuicListenerAnswer,
85
+ quicListenerPaceTime,
86
+ quicListenerTakeFlight,
87
+ } from "nish/net/quic-listener"
88
+ import { TlsP256Signer } from "nish/net/tls"
89
+
90
+ /** The timer wheel's buckets, one millisecond each: its horizon. */
91
+ export const H3_SERVER_WHEEL: i32 = 2048
92
+
93
+ /** The most datagrams one `receive` reads, so one busy socket cannot hold the loop. */
94
+ export const H3_SERVER_RECEIVE_BURST: i32 = 256
95
+
96
+ /** The answers to datagrams no slot owns that may go at once, and the milliseconds that earn one more. */
97
+ export const H3_SERVER_ANSWER_BURST: i32 = 16
98
+ export const H3_SERVER_ANSWER_INTERVAL: i64 = 100
99
+
100
+ /** The address form `nish:net` reads and writes. */
101
+ const H3_SERVER_ADDRESS: i32 = 18
102
+
103
+ /** What `udpSendTo` answers where the platform has no segmentation offload. */
104
+ const H3_SERVER_NO_GSO: i32 = -95
105
+
106
+ /** The largest datagram read: a GRO receive of up to 64 KB. */
107
+ const H3_SERVER_RECEIVE_SIZE: i32 = 65536
108
+
109
+ /** Index entries per slot: its local connection IDs, then the client's original DCID. */
110
+ const H3_SERVER_KEYS: i32 = QUIC_CONN_LOCAL_CIDS + 1
111
+
112
+ /** Typed constants, since a bare literal is an `f64` under `--number-mode f64`. */
113
+ const H3_SERVER_ZERO: i32 = 0
114
+ const H3_SERVER_NONE: i64 = -1
115
+
116
+ /** The index hash's masks, which keep its shifts from overflowing. */
117
+ const H3_SERVER_MASK_LOW: i64 = 0x3fffffffffff
118
+ const H3_SERVER_MASK: i64 = 0x7fffffffffff
119
+
120
+ /** A slot's states. */
121
+ const H3_SERVER_FREE: i32 = 0
122
+ const H3_SERVER_BUSY: i32 = 1
123
+
124
+ /** The bucket of `time` on the wheel. */
125
+ const http3ServerBucket = (time: i64): i32 => toI32(time % toI64(H3_SERVER_WHEEL))
126
+
127
+ /**
128
+ * The connection IDs of every slot, by hash: open addressing over
129
+ * `table`, whose entries are an index into `keys` plus one. Entry `e`
130
+ * belongs to slot `e / H3_SERVER_KEYS`.
131
+ */
132
+ export class Http3CidIndex {
133
+ table: i32[]
134
+ /** Entry `e`'s ID is `keys[e * 20 .. e * 20 + lengths[e])`; a length of 0 is no entry. */
135
+ keys: u8[]
136
+ lengths: i32[]
137
+ /** The hash's key: eight bytes from the listener's entropy. */
138
+ salt: u8[]
139
+
140
+ constructor(slots: i32, salt: u8[]) {
141
+ const entries: i32 = slots * H3_SERVER_KEYS
142
+ let size: i32 = 8
143
+ while (size < entries * 2) {
144
+ size = size * 2
145
+ }
146
+ this.table = new Array<i32>(size)
147
+ this.keys = new Array<u8>(entries * QUIC_MAX_CID_LENGTH)
148
+ this.lengths = new Array<i32>(entries)
149
+ this.salt = new Array<u8>(8)
150
+ for (let k: i32 = 0; k < 8 && k < toI32(salt.length); k++) {
151
+ this.salt[k] = salt[k]
152
+ }
153
+ }
154
+
155
+ /** The bucket the ID `buf[at .. at + length)` starts its probe at. */
156
+ bucket(buf: u8[], at: i32, length: i32): i32 {
157
+ let h: i64 = toI64(length)
158
+ for (let k: i32 = 0; k < length && at + k >= 0 && at + k < toI32(buf.length); k++) {
159
+ const j: i32 = k & 7
160
+ const mixed: i64 = toI64(toI32(buf[at + k]) ^ toI32(this.salt[j]))
161
+ h = (((h << 7) & H3_SERVER_MASK_LOW) ^ (h >> 29) ^ mixed) & H3_SERVER_MASK
162
+ }
163
+ h = h ^ (h >> 17) ^ (h >> 31)
164
+ return toI32(h & toI64(toI32(this.table.length) - 1))
165
+ }
166
+
167
+ /** Whether entry `e` holds the ID `buf[at .. at + length)`. */
168
+ holds(e: i32, buf: u8[], at: i32, length: i32): boolean {
169
+ if (e < 0 || e >= toI32(this.lengths.length) || this.lengths[e] !== length) {
170
+ return false
171
+ }
172
+ const base: i32 = e * QUIC_MAX_CID_LENGTH
173
+ for (let k: i32 = 0; k < length; k++) {
174
+ if (at + k < 0 || at + k >= toI32(buf.length) || this.keys[base + k] !== buf[at + k]) {
175
+ return false
176
+ }
177
+ }
178
+ return true
179
+ }
180
+
181
+ /** The slot owning the ID `buf[at .. at + length)`, or -1. */
182
+ find(buf: u8[], at: i32, length: i32): i32 {
183
+ const size: i32 = toI32(this.table.length)
184
+ let h: i32 = this.bucket(buf, at, length)
185
+ for (let probe: i32 = 0; probe < size && h >= 0 && h < size; probe++) {
186
+ const e: i32 = this.table[h] - 1
187
+ if (e < 0) {
188
+ return -1
189
+ }
190
+ if (this.holds(e, buf, at, length)) {
191
+ return e / H3_SERVER_KEYS
192
+ }
193
+ h = (h + 1) & (size - 1)
194
+ }
195
+ return -1
196
+ }
197
+
198
+ /** Files entry `e` as the ID `buf[at .. at + length)`; an ID already filed under another entry is left there. */
199
+ insert(e: i32, buf: u8[], at: i32, length: i32): void {
200
+ if (length <= 0 || length > QUIC_MAX_CID_LENGTH || e < 0 || e >= toI32(this.lengths.length)) {
201
+ return
202
+ }
203
+ if (this.find(buf, at, length) >= 0) {
204
+ return
205
+ }
206
+ const base: i32 = e * QUIC_MAX_CID_LENGTH
207
+ for (let k: i32 = 0; k < length && at + k >= 0 && at + k < toI32(buf.length); k++) {
208
+ this.keys[base + k] = buf[at + k]
209
+ }
210
+ this.lengths[e] = length
211
+ const size: i32 = toI32(this.table.length)
212
+ let h: i32 = this.bucket(buf, at, length)
213
+ for (let probe: i32 = 0; probe < size && h >= 0 && h < size; probe++) {
214
+ if (this.table[h] === 0) {
215
+ this.table[h] = e + 1
216
+ return
217
+ }
218
+ h = (h + 1) & (size - 1)
219
+ }
220
+ }
221
+
222
+ /** Takes entry `e` out, moving later entries of its probe run back so none is lost. */
223
+ remove(e: i32): void {
224
+ if (e < 0 || e >= toI32(this.lengths.length) || this.lengths[e] === 0) {
225
+ return
226
+ }
227
+ const size: i32 = toI32(this.table.length)
228
+ const mask: i32 = size - 1
229
+ const base: i32 = e * QUIC_MAX_CID_LENGTH
230
+ let h: i32 = this.bucket(this.keys, base, this.lengths[e])
231
+ let hole: i32 = -1
232
+ for (let probe: i32 = 0; probe < size && h >= 0 && h < size; probe++) {
233
+ if (this.table[h] === 0) {
234
+ break
235
+ }
236
+ if (this.table[h] === e + 1) {
237
+ hole = h
238
+ break
239
+ }
240
+ h = (h + 1) & mask
241
+ }
242
+ this.lengths[e] = 0
243
+ if (hole < 0) {
244
+ return
245
+ }
246
+ let next: i32 = (hole + 1) & mask
247
+ for (let probe: i32 = 0; probe < size && next >= 0 && next < size && hole >= 0 && hole < size; probe++) {
248
+ const other: i32 = this.table[next] - 1
249
+ if (other < 0) {
250
+ break
251
+ }
252
+ const home: i32 = this.bucket(this.keys, other * QUIC_MAX_CID_LENGTH, this.lengths[other])
253
+ // The entry may move into the hole when its home is not between the hole and it.
254
+ if (((hole - home) & mask) < ((next - home) & mask)) {
255
+ this.table[hole] = this.table[next]
256
+ hole = next
257
+ }
258
+ next = (next + 1) & mask
259
+ }
260
+ if (hole >= 0 && hole < size) {
261
+ this.table[hole] = 0
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Slots filed by the millisecond of their next deadline: `head[b]` is a
268
+ * bucket's first slot plus one, `nextOf` and `prevOf` link a bucket's slots,
269
+ * and `bits` marks the buckets that hold any.
270
+ */
271
+ export class Http3Wheel {
272
+ head: i32[]
273
+ nextOf: i32[]
274
+ prevOf: i32[]
275
+ /** The bucket a slot is filed in, or -1, and the deadline it was filed for. */
276
+ bucketOf: i32[]
277
+ dueAt: i64[]
278
+ bits: i64[]
279
+ /** Every bucket before this time has been run. */
280
+ cursor: i64 = -1
281
+
282
+ constructor(slots: i32) {
283
+ this.head = new Array<i32>(H3_SERVER_WHEEL)
284
+ this.nextOf = new Array<i32>(slots)
285
+ this.prevOf = new Array<i32>(slots)
286
+ this.bucketOf = new Array<i32>(slots)
287
+ this.bucketOf.fill(-1)
288
+ this.dueAt = new Array<i64>(slots)
289
+ this.bits = new Array<i64>(H3_SERVER_WHEEL / 64)
290
+ }
291
+
292
+ /** Marks bucket `b` filled or not. */
293
+ flag(b: i32, on: boolean): void {
294
+ const word: i32 = b >> 6
295
+ if (word < 0 || word >= toI32(this.bits.length)) {
296
+ return
297
+ }
298
+ const bit: i64 = toI64(1) << toI64(b & 63)
299
+ this.bits[word] = on ? this.bits[word] | bit : this.bits[word] & ~bit
300
+ }
301
+
302
+ /** Takes `slot` out of its bucket. */
303
+ unfile(slot: i32): void {
304
+ if (slot < 0 || slot >= toI32(this.bucketOf.length)) {
305
+ return
306
+ }
307
+ const b: i32 = this.bucketOf[slot]
308
+ if (b < 0 || b >= H3_SERVER_WHEEL) {
309
+ return
310
+ }
311
+ const prev: i32 = this.prevOf[slot]
312
+ const next: i32 = this.nextOf[slot]
313
+ if (prev >= 0 && prev < toI32(this.nextOf.length)) {
314
+ this.nextOf[prev] = next
315
+ } else {
316
+ this.head[b] = next + 1
317
+ }
318
+ if (next >= 0 && next < toI32(this.prevOf.length)) {
319
+ this.prevOf[next] = prev
320
+ }
321
+ if (this.head[b] === 0) {
322
+ this.flag(b, false)
323
+ }
324
+ this.bucketOf[slot] = -1
325
+ }
326
+
327
+ /** Files `slot` under `due`, kept inside the wheel's horizon from `now`; a negative `due` files nothing. */
328
+ file(slot: i32, due: i64, now: i64): void {
329
+ this.unfile(slot)
330
+ if (due < 0 || slot < 0 || slot >= toI32(this.bucketOf.length)) {
331
+ return
332
+ }
333
+ if (this.cursor < 0) {
334
+ this.cursor = now
335
+ }
336
+ let at: i64 = due < this.cursor ? this.cursor : due
337
+ const horizon: i64 = this.cursor + toI64(H3_SERVER_WHEEL - 1)
338
+ if (at > horizon) {
339
+ at = horizon
340
+ }
341
+ const b: i32 = http3ServerBucket(at)
342
+ if (b < 0 || b >= H3_SERVER_WHEEL) {
343
+ return
344
+ }
345
+ const first: i32 = this.head[b] - 1
346
+ this.nextOf[slot] = first
347
+ this.prevOf[slot] = -1
348
+ if (first >= 0 && first < toI32(this.prevOf.length)) {
349
+ this.prevOf[first] = slot
350
+ }
351
+ this.head[b] = slot + 1
352
+ this.bucketOf[slot] = b
353
+ this.dueAt[slot] = due
354
+ this.flag(b, true)
355
+ }
356
+
357
+ /** The slot at the head of the bucket for `time`, taken out of it, or -1. */
358
+ take(time: i64): i32 {
359
+ const b: i32 = http3ServerBucket(time)
360
+ const slot: i32 = b >= 0 && b < H3_SERVER_WHEEL ? this.head[b] - 1 : -1
361
+ if (slot >= 0) {
362
+ this.unfile(slot)
363
+ }
364
+ return slot
365
+ }
366
+
367
+ /** Milliseconds from `now` to the first filled bucket, at most the horizon, or -1 when none is. */
368
+ next(now: i64): i32 {
369
+ // From the cursor: a bucket before it has run, and one at `now` after a
370
+ // tick is the horizon's, a full turn of the wheel away.
371
+ const from: i64 = this.cursor < 0 ? now : this.cursor
372
+ const end: i64 = from + toI64(H3_SERVER_WHEEL)
373
+ let time: i64 = from
374
+ while (time < end) {
375
+ const b: i32 = http3ServerBucket(time)
376
+ const word: i32 = b >> 6
377
+ if (word < 0 || word >= toI32(this.bits.length)) {
378
+ return -1
379
+ }
380
+ if (this.bits[word] === 0) {
381
+ // An empty word: on to the next one.
382
+ const word64: i32 = 64
383
+ time = time + toI64(word64 - (b & 63))
384
+ } else if (this.head[b] !== 0) {
385
+ return time <= now ? H3_SERVER_ZERO : toI32(time - now)
386
+ } else {
387
+ time = time + 1
388
+ }
389
+ }
390
+ return -1
391
+ }
392
+ }
393
+
394
+ /**
395
+ * The pool. `slot`s are indices from 0 to `size() - 1`; `connection(slot)`
396
+ * is the HTTP/3 side of each and `quic(slot)` the QUIC side.
397
+ */
398
+ export class Http3Server {
399
+ quicConfig: QuicServerConfig
400
+ listener: QuicListener
401
+ /** What the listener answers into, made once: its reply and IDs are refilled in their own room. */
402
+ answer: QuicListenerAnswer
403
+ quics: QuicConnection[]
404
+ conns: Http3Connection[]
405
+ index: Http3CidIndex
406
+ wheel: Http3Wheel
407
+ /** Each slot's client address, as its first datagram came from. */
408
+ addresses: u8[][]
409
+ /** What `receive` reads into, and where `flush` writes a flight. */
410
+ rx: u8[]
411
+ tx: u8[]
412
+ from: u8[]
413
+ meta: i32[]
414
+ entropy: u8[]
415
+ flight: QuicFlight
416
+ /** Signs every slot's CertificateVerify in `serve`, one for the server, keeping nothing per signature. */
417
+ signer: TlsP256Signer
418
+ /** Each slot's connection-ID index signature, so it is refiled only when its IDs changed. */
419
+ signatures: i64[]
420
+ state: i32[]
421
+ /** The slots with datagrams for the program, a ring, and the slots with something to send, a stack. */
422
+ readyRing: i32[]
423
+ dirtyStack: i32[]
424
+ /** Free slots, a stack. */
425
+ free: i32[]
426
+ /** Per slot: 1 while on the ready ring, 2 while on the dirty stack. */
427
+ queued: i32[]
428
+ /** When the answer budget was last topped up, -1 before the first answer. */
429
+ answerRefilled: i64 = -1
430
+ fd: i32 = -1
431
+ readyHead: i32 = 0
432
+ readyCount: i32 = 0
433
+ dirtyCount: i32 = 0
434
+ freeCount: i32 = 0
435
+ /** What is left of the answer budget (see `answerRefilled`). */
436
+ answerBudget: i32 = 0
437
+ /** Counters for a log or a test, saturating: connections accepted, Initials dropped for want of a slot, datagrams past the answer budget, sends that failed. */
438
+ accepted: i32 = 0
439
+ refused: i32 = 0
440
+ limited: i32 = 0
441
+ sendErrors: i32 = 0
442
+
443
+ /**
444
+ * A pool of `size` slots serving HTTP/3 under `config` over QUIC under
445
+ * `quicConfig` on the UDP socket `fd`. `listenerEntropy` is the
446
+ * listener's `QUIC_LISTENER_ENTROPY_SIZE` random bytes, whose first eight
447
+ * also key the connection-ID index (wiped in the caller's array, as the
448
+ * listener does). Every slot's connections are made here.
449
+ */
450
+ constructor(quicConfig: QuicServerConfig, config: Http3Config, fd: i32, size: i32, listenerEntropy: u8[]) {
451
+ if (size < 1 || size > 65536) {
452
+ panic(`Http3Server: ${size} slots, outside 1 to 65536`)
453
+ }
454
+ this.quicConfig = quicConfig
455
+ this.fd = fd
456
+ this.index = new Http3CidIndex(size, listenerEntropy)
457
+ this.listener = new QuicListener(quicConfig, listenerEntropy)
458
+ this.answer = new QuicListenerAnswer(QUIC_LISTENER_ANSWER_SIZE)
459
+ this.wheel = new Http3Wheel(size)
460
+ this.quics = []
461
+ this.conns = []
462
+ this.addresses = []
463
+ this.entropy = new Array<u8>(QUIC_CONN_ENTROPY_SIZE)
464
+ for (let k: i32 = 0; k < size; k++) {
465
+ crypto.getRandomValues(this.entropy)
466
+ const quic = new QuicConnection(quicConfig, this.entropy)
467
+ this.quics.push(quic)
468
+ this.conns.push(new Http3Connection(config, quic))
469
+ this.addresses.push(new Array<u8>(H3_SERVER_ADDRESS))
470
+ }
471
+ this.rx = new Array<u8>(H3_SERVER_RECEIVE_SIZE)
472
+ this.tx = new Array<u8>(QUIC_LISTENER_FLIGHT_MAX * QUIC_CONN_DATAGRAM_SIZE)
473
+ this.from = new Array<u8>(H3_SERVER_ADDRESS)
474
+ this.meta = [H3_SERVER_ZERO, H3_SERVER_ZERO]
475
+ this.flight = new QuicFlight()
476
+ this.signer = new TlsP256Signer()
477
+ this.signatures = new Array<i64>(size)
478
+ this.state = new Array<i32>(size)
479
+ this.readyRing = new Array<i32>(size)
480
+ this.dirtyStack = new Array<i32>(size)
481
+ this.free = new Array<i32>(size)
482
+ this.queued = new Array<i32>(size)
483
+ for (let k: i32 = size - 1; k >= 0; k--) {
484
+ this.free[this.freeCount] = k
485
+ this.freeCount = this.freeCount + 1
486
+ }
487
+ }
488
+
489
+ /** How many slots the pool has. */
490
+ size(): i32 {
491
+ return toI32(this.quics.length)
492
+ }
493
+
494
+ /** How many slots hold a connection. */
495
+ busy(): i32 {
496
+ return this.size() - this.freeCount
497
+ }
498
+
499
+ /** Whether `slot` holds a connection. */
500
+ holds(slot: i32): boolean {
501
+ return slot >= 0 && slot < toI32(this.state.length) && this.state[slot] === H3_SERVER_BUSY
502
+ }
503
+
504
+ /** The HTTP/3 connection of `slot`; a slot out of range names the first. */
505
+ connection(slot: i32): Http3Connection {
506
+ const at: i32 = slot >= 0 && slot < toI32(this.conns.length) ? slot : H3_SERVER_ZERO
507
+ return this.conns[at]
508
+ }
509
+
510
+ /** The QUIC connection of `slot`; a slot out of range names the first. */
511
+ quic(slot: i32): QuicConnection {
512
+ const at: i32 = slot >= 0 && slot < toI32(this.quics.length) ? slot : H3_SERVER_ZERO
513
+ return this.quics[at]
514
+ }
515
+
516
+ /** Puts `slot` on the ready ring and the dirty stack, unless it is on them. */
517
+ touch(slot: i32): void {
518
+ if (!this.holds(slot)) {
519
+ return
520
+ }
521
+ if ((this.queued[slot] & 1) === 0 && this.readyCount < toI32(this.readyRing.length)) {
522
+ const at: i32 = (this.readyHead + this.readyCount) % toI32(this.readyRing.length)
523
+ this.readyRing[at] = slot
524
+ this.readyCount = this.readyCount + 1
525
+ this.queued[slot] = this.queued[slot] | 1
526
+ }
527
+ this.dirty(slot)
528
+ }
529
+
530
+ /** Puts `slot` on the dirty stack, for `flush`. */
531
+ dirty(slot: i32): void {
532
+ if ((this.queued[slot] & 2) === 0 && this.dirtyCount < toI32(this.dirtyStack.length)) {
533
+ this.dirtyStack[this.dirtyCount] = slot
534
+ this.dirtyCount = this.dirtyCount + 1
535
+ this.queued[slot] = this.queued[slot] | 2
536
+ }
537
+ }
538
+
539
+ /** The next slot with news for the program, or -1: call its connection's `next` until it has none. */
540
+ ready(): i32 {
541
+ while (this.readyCount > 0) {
542
+ const slot: i32 = this.readyRing[this.readyHead]
543
+ this.readyHead = (this.readyHead + 1) % toI32(this.readyRing.length)
544
+ this.readyCount = this.readyCount - 1
545
+ this.queued[slot] = this.queued[slot] & ~1
546
+ if (this.holds(slot)) {
547
+ return slot
548
+ }
549
+ }
550
+ return -1
551
+ }
552
+
553
+ /**
554
+ * Reads every datagram waiting on the socket, at most
555
+ * `H3_SERVER_RECEIVE_BURST`, and hands each to its slot or the listener,
556
+ * signing a handshake's CertificateVerify with `key` (ECDSA P-256) when one
557
+ * asks. A GRO receive is cut into its datagrams in place. Answers how many
558
+ * datagrams it read.
559
+ */
560
+ receive(now: i64, key: Secret<u8[]>): i32 {
561
+ let count: i32 = 0
562
+ for (let burst: i32 = 0; burst < H3_SERVER_RECEIVE_BURST; burst++) {
563
+ const n: i32 = udpRecvFrom(
564
+ this.fd,
565
+ this.rx,
566
+ H3_SERVER_ZERO,
567
+ H3_SERVER_RECEIVE_SIZE,
568
+ this.from,
569
+ this.meta
570
+ )
571
+ if (n < 0) {
572
+ return count
573
+ }
574
+ const segment: i32 = this.meta[0] > 0 ? this.meta[0] : n
575
+ let at: i32 = 0
576
+ while (at < n && segment > 0) {
577
+ const length: i32 = n - at < segment ? n - at : segment
578
+ this.datagram(this.rx, at, length, this.from, now, key)
579
+ count = count + 1
580
+ at = at + length
581
+ }
582
+ }
583
+ return count
584
+ }
585
+
586
+ /**
587
+ * One datagram, `buf[at .. at + len)`, from `address`: to the slot that
588
+ * owns its first packet's Destination Connection ID, or to the listener.
589
+ */
590
+ datagram(buf: u8[], at: i32, len: i32, address: u8[], now: i64, key: Secret<u8[]>): void {
591
+ if (len < 1 || at < 0 || at + len > toI32(buf.length)) {
592
+ return
593
+ }
594
+ // RFC 8999: a long header carries the DCID's length; a short one is ours, of our length.
595
+ let dcidAt: i32 = at + 1
596
+ let dcidLength: i32 = QUIC_CONN_CID_LENGTH
597
+ if ((toI32(buf[at]) & 0x80) !== 0) {
598
+ if (len < 6) {
599
+ return
600
+ }
601
+ dcidLength = toI32(buf[at + 5])
602
+ dcidAt = at + 6
603
+ }
604
+ if (dcidLength > QUIC_MAX_CID_LENGTH || dcidAt + dcidLength > at + len) {
605
+ return
606
+ }
607
+ const slot: i32 = this.index.find(buf, dcidAt, dcidLength)
608
+ if (slot >= 0) {
609
+ this.serve(slot, buf, at, len, now, key)
610
+ return
611
+ }
612
+ this.unowned(buf, at, len, address, now, key)
613
+ }
614
+
615
+ /**
616
+ * A datagram no slot owns: the listener decides, reading it where it lies
617
+ * in `buf` and answering into `answer`, and may make a connection in a
618
+ * free slot, which reads the same window. Nothing is copied (H3-3).
619
+ */
620
+ unowned(buf: u8[], at: i32, len: i32, address: u8[], now: i64, key: Secret<u8[]>): void {
621
+ if (!this.worthListening(buf, at, len, now)) {
622
+ return
623
+ }
624
+ const answer: QuicListenerAnswer = this.answer
625
+ if (this.listener.handleWindow(buf, at, len, address, now, answer) !== QUIC_LISTEN_ACCEPT) {
626
+ const reply: i32 = toI32(answer.reply.length)
627
+ if (reply > 0) {
628
+ this.send(answer.reply, H3_SERVER_ZERO, reply, address, H3_SERVER_ZERO)
629
+ }
630
+ return
631
+ }
632
+ if (this.freeCount <= 0) {
633
+ return
634
+ }
635
+ this.freeCount = this.freeCount - 1
636
+ const slot: i32 = this.free[this.freeCount]
637
+ crypto.getRandomValues(this.entropy)
638
+ const quic: QuicConnection = this.quics[slot]
639
+ quic.reset(this.entropy)
640
+ this.conns[slot].restart()
641
+ if (answer.retried) {
642
+ quic.acceptRetry(answer.originalDcid, answer.retryScid)
643
+ }
644
+ const to: u8[] = this.addresses[slot]
645
+ for (let k: i32 = 0; k < toI32(to.length) && k < toI32(address.length); k++) {
646
+ to[k] = address[k]
647
+ }
648
+ this.state[slot] = H3_SERVER_BUSY
649
+ this.signatures[slot] = H3_SERVER_NONE
650
+ this.accepted = h3Count(this.accepted)
651
+ this.serve(slot, buf, at, len, now, key)
652
+ }
653
+
654
+ /**
655
+ * Whether the listener could answer `buf[at .. at + len)` at all, read
656
+ * from its first bytes: what it would drop without a word is dropped
657
+ * here, before the listener parses it. That is a short header too short for a stateless reset; a long
658
+ * one shorter than a full Initial, which neither Version Negotiation nor a
659
+ * new connection answers (RFC 9000 §14.1, §6.1); version 0; a version 1
660
+ * packet that is not an Initial, or whose DCID is under 8 bytes (§7.2);
661
+ * and an Initial when no slot is free, which is counted in `refused`.
662
+ * A stateless reset and a Version Negotiation packet, the answers that do
663
+ * not start a connection, are also held to `takeAnswer`'s budget.
664
+ */
665
+ worthListening(buf: u8[], at: i32, len: i32, now: i64): boolean {
666
+ const first: i32 = toI32(buf[at])
667
+ if ((first & 0x80) === 0) {
668
+ return len > QUIC_LISTENER_MIN_RESET && this.takeAnswer(now)
669
+ }
670
+ if (len < QUIC_CONN_DATAGRAM_SIZE) {
671
+ return false
672
+ }
673
+ let version: i32 = 0
674
+ for (let k: i32 = 1; k <= 4; k++) {
675
+ version = (version << 8) | toI32(buf[at + k])
676
+ }
677
+ if (version !== 1) {
678
+ return version !== 0 && this.takeAnswer(now)
679
+ }
680
+ if ((first & 0x30) !== 0 || toI32(buf[at + 5]) < 8) {
681
+ return false
682
+ }
683
+ if (this.freeCount <= 0) {
684
+ this.refused = h3Count(this.refused)
685
+ return false
686
+ }
687
+ return true
688
+ }
689
+
690
+ /**
691
+ * Takes one answer to a datagram no slot owns from the budget —
692
+ * `H3_SERVER_ANSWER_BURST` at once, then one per
693
+ * `H3_SERVER_ANSWER_INTERVAL` milliseconds — topping it up first. Past it
694
+ * the datagram is dropped before the listener sees it, so a flood of
695
+ * garbage costs no answer and no MAC past the budget (H3-3); counted in `limited`.
696
+ */
697
+ takeAnswer(now: i64): boolean {
698
+ if (this.answerRefilled < 0 || now < this.answerRefilled) {
699
+ this.answerRefilled = now
700
+ this.answerBudget = H3_SERVER_ANSWER_BURST
701
+ }
702
+ const earned: i64 = (now - this.answerRefilled) / H3_SERVER_ANSWER_INTERVAL
703
+ if (earned > 0) {
704
+ const room: i64 = toI64(H3_SERVER_ANSWER_BURST - this.answerBudget)
705
+ this.answerBudget = this.answerBudget + toI32(earned < room ? earned : room)
706
+ this.answerRefilled = this.answerRefilled + earned * H3_SERVER_ANSWER_INTERVAL
707
+ }
708
+ if (this.answerBudget <= 0) {
709
+ this.limited = h3Count(this.limited)
710
+ return false
711
+ }
712
+ this.answerBudget = this.answerBudget - 1
713
+ return true
714
+ }
715
+
716
+ /** Hands `slot`'s connection a datagram, signs when the handshake asks, and refiles its IDs and timer. */
717
+ serve(slot: i32, buf: u8[], at: i32, len: i32, now: i64, key: Secret<u8[]>): void {
718
+ const quic: QuicConnection = this.quics[slot]
719
+ quic.receiveWindow(buf, at, len, now)
720
+ const input: u8[] | null = quic.signatureInput()
721
+ if (input !== null) {
722
+ const signature: u8[] | null = this.signer.sign(key, input)
723
+ if (signature !== null) {
724
+ quic.sign(signature)
725
+ }
726
+ }
727
+ this.refile(slot)
728
+ this.touch(slot)
729
+ }
730
+
731
+ /** Files `slot`'s connection IDs in the index again when they changed. */
732
+ refile(slot: i32): void {
733
+ const quic: QuicConnection = this.quics[slot]
734
+ const signature: i64 =
735
+ (quic.cids.nextLocal << 16) |
736
+ (toI64(quic.cids.activeLocal()) << 8) |
737
+ (toI64(quic.originalDcidLength) << 1) |
738
+ (quic.handshakeComplete ? toI64(1) : toI64(0))
739
+ if (signature === this.signatures[slot]) {
740
+ return
741
+ }
742
+ this.signatures[slot] = signature
743
+ this.unindex(slot)
744
+ const first: i32 = slot * H3_SERVER_KEYS
745
+ for (let j: i32 = 0; j < QUIC_CONN_LOCAL_CIDS && j < toI32(quic.cids.local.length); j++) {
746
+ const entry = quic.cids.local[j]
747
+ if (entry.used) {
748
+ this.index.insert(first + j, entry.cid, H3_SERVER_ZERO, entry.length)
749
+ }
750
+ }
751
+ if (!quic.handshakeComplete && quic.originalDcidLength > 0) {
752
+ this.index.insert(
753
+ first + QUIC_CONN_LOCAL_CIDS,
754
+ quic.originalDcid,
755
+ H3_SERVER_ZERO,
756
+ quic.originalDcidLength
757
+ )
758
+ }
759
+ }
760
+
761
+ /** Sends `buf[at .. at + len)` to `to`, cut into `segment`-byte datagrams when it is above 0; answers whether it went. */
762
+ send(buf: u8[], at: i32, len: i32, to: u8[], segment: i32): boolean {
763
+ const sent: i32 = udpSendTo(this.fd, buf, at, len, to, segment, H3_SERVER_ZERO)
764
+ if (sent >= 0) {
765
+ return true
766
+ }
767
+ if (sent !== H3_SERVER_NO_GSO || segment <= 0) {
768
+ this.sendErrors = h3Count(this.sendErrors)
769
+ return false
770
+ }
771
+ // No segmentation offload here: one datagram at a time.
772
+ let p: i32 = at
773
+ while (p < at + len) {
774
+ const n: i32 = at + len - p < segment ? at + len - p : segment
775
+ if (udpSendTo(this.fd, buf, p, n, to, H3_SERVER_ZERO, H3_SERVER_ZERO) < 0) {
776
+ this.sendErrors = h3Count(this.sendErrors)
777
+ return false
778
+ }
779
+ p = p + n
780
+ }
781
+ return true
782
+ }
783
+
784
+ /**
785
+ * Sends what every touched slot has, as far as its pacer lets it, closes a
786
+ * slot whose HTTP/3 side is done after GOAWAY, frees a slot whose
787
+ * connection has closed, and files each in the wheel under its next
788
+ * deadline.
789
+ */
790
+ flush(now: i64): void {
791
+ while (this.dirtyCount > 0) {
792
+ this.dirtyCount = this.dirtyCount - 1
793
+ const slot: i32 = this.dirtyStack[this.dirtyCount]
794
+ this.queued[slot] = this.queued[slot] & ~2
795
+ if (this.holds(slot)) {
796
+ this.flushSlot(slot, now)
797
+ }
798
+ }
799
+ }
800
+
801
+ /** `flush` of one slot. */
802
+ flushSlot(slot: i32, now: i64): void {
803
+ const quic: QuicConnection = this.quics[slot]
804
+ if (this.conns[slot].isDone() && !quic.closed()) {
805
+ quic.close(H3_NO_ERROR)
806
+ }
807
+ const to: u8[] = this.addresses[slot]
808
+ let retried: boolean = false
809
+ for (let guard: i32 = 0; guard < 64; guard++) {
810
+ const n: i32 = quicListenerTakeFlight(
811
+ quic,
812
+ now,
813
+ this.tx,
814
+ H3_SERVER_ZERO,
815
+ toI32(this.tx.length),
816
+ this.flight
817
+ )
818
+ if (n <= 0 && !retried) {
819
+ // Asked once more: a stream whose lost bytes were all acknowledged
820
+ // since gives up its new ones in the packet that finds that out (H3-7).
821
+ retried = true
822
+ continue
823
+ }
824
+ if (n <= 0) {
825
+ break
826
+ }
827
+ const segment: i32 = this.flight.count > 1 ? this.flight.segment : H3_SERVER_ZERO
828
+ if (!this.send(this.tx, H3_SERVER_ZERO, n, to, segment)) {
829
+ break
830
+ }
831
+ retried = false
832
+ }
833
+ if (quic.closed()) {
834
+ // The CONNECTION_CLOSE goes whatever the pacer says: nothing follows it.
835
+ const n: i32 = quic.takeDatagramInto(this.tx, H3_SERVER_ZERO, now)
836
+ if (n > 0) {
837
+ this.send(this.tx, H3_SERVER_ZERO, n, to, H3_SERVER_ZERO)
838
+ }
839
+ this.release(slot)
840
+ return
841
+ }
842
+ this.refile(slot)
843
+ // The pacer answers a time, not a delay: the slot wakes at it when it
844
+ // comes before the connection's own deadline.
845
+ let due: i64 = quic.deadline()
846
+ const paced: i64 = quicListenerPaceTime(quic, now)
847
+ if (paced > now && (due < 0 || paced < due)) {
848
+ due = paced
849
+ }
850
+ this.wheel.file(slot, due, now)
851
+ }
852
+
853
+ /** Takes every one of `slot`'s connection IDs out of the index. */
854
+ unindex(slot: i32): void {
855
+ const first: i32 = slot * H3_SERVER_KEYS
856
+ for (let j: i32 = 0; j < H3_SERVER_KEYS; j++) {
857
+ this.index.remove(first + j)
858
+ }
859
+ }
860
+
861
+ /** Frees `slot`: its IDs leave the index and its timer the wheel. */
862
+ release(slot: i32): void {
863
+ this.unindex(slot)
864
+ this.wheel.unfile(slot)
865
+ this.quics[slot].release()
866
+ this.state[slot] = H3_SERVER_FREE
867
+ if (this.freeCount < toI32(this.free.length)) {
868
+ this.free[this.freeCount] = slot
869
+ this.freeCount = this.freeCount + 1
870
+ }
871
+ }
872
+
873
+ /** Runs the timers due by `now`, and marks each slot it ran for `flush`. */
874
+ tick(now: i64): void {
875
+ const wheel: Http3Wheel = this.wheel
876
+ if (wheel.cursor < 0) {
877
+ wheel.cursor = now
878
+ }
879
+ // Late by more than a turn: every bucket is run once, from a turn ago, so a
880
+ // slot filed again lands after `now` and not behind the cursor.
881
+ if (now - wheel.cursor >= toI64(H3_SERVER_WHEEL)) {
882
+ wheel.cursor = now - toI64(H3_SERVER_WHEEL - 1)
883
+ }
884
+ let steps: i32 = 0
885
+ while (wheel.cursor <= now && steps < H3_SERVER_WHEEL) {
886
+ let slot: i32 = wheel.take(wheel.cursor)
887
+ while (slot >= 0) {
888
+ if (this.holds(slot)) {
889
+ if (wheel.dueAt[slot] > now) {
890
+ // Filed at the horizon: not due yet, so filed again.
891
+ wheel.file(slot, wheel.dueAt[slot], now)
892
+ } else {
893
+ this.quics[slot].handleTimer(now)
894
+ this.dirty(slot)
895
+ }
896
+ }
897
+ slot = wheel.take(wheel.cursor)
898
+ }
899
+ wheel.cursor = wheel.cursor + 1
900
+ steps++
901
+ }
902
+ if (wheel.cursor <= now) {
903
+ wheel.cursor = now + 1
904
+ }
905
+ }
906
+
907
+ /** Milliseconds until `tick` has a timer to run, for `pollWait`; -1 when no slot has one. */
908
+ timeout(now: i64): i32 {
909
+ return this.wheel.next(now)
910
+ }
911
+ }