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