@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.
- package/INSTALL.md +43 -34
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +50 -13
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +43 -24
- package/runtime/runtime-net.c +57 -10
- package/runtime/runtime-os.c +62 -20
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +12 -0
- package/runtime/runtime.c +66 -18
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +14 -12
- package/std/README.md +168 -36
- package/std/crypto/aes.ts +51 -8
- package/std/crypto/base64url.ts +25 -12
- package/std/crypto/chacha20poly1305.ts +43 -29
- package/std/crypto/ct.ts +6 -1
- package/std/crypto/hkdf.ts +364 -3
- package/std/crypto/hmac.ts +307 -7
- package/std/crypto/p256.ts +618 -34
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +3 -2
- package/std/crypto/x25519.ts +88 -36
- package/std/crypto/x509.ts +130 -47
- package/std/json.ts +297 -41
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +911 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +462 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +929 -0
- package/std/net/quic-packet.ts +1698 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1609 -0
- package/std/net/quic.ts +3578 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +414 -0
- package/std/net/tls.ts +1437 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
- 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
|
+
}
|