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