@amritk/nish-x86_64-linux 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +30 -7
- 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-wasm.c +1 -0
- package/runtime/runtime.c +46 -8
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +1 -3
- package/std/README.md +166 -35
- 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 +148 -27
- 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/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 +905 -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 +392 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +653 -0
- package/std/net/quic-packet.ts +1474 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1603 -0
- package/std/net/quic.ts +3214 -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 +410 -0
- package/std/net/tls.ts +1368 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
package/std/net/http3.ts
ADDED
|
@@ -0,0 +1,2145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/http3` — the server side of an HTTP/3 connection (RFC 9114) on
|
|
3
|
+
* the streams of a `QuicConnection` (`nish/net/quic`): the control stream
|
|
4
|
+
* and SETTINGS both ways, the QPACK encoder and decoder streams (opened, at
|
|
5
|
+
* dynamic table capacity 0), requests and responses on bidirectional streams
|
|
6
|
+
* with their bodies streamed both ways, GOAWAY, and RFC 9114's error codes.
|
|
7
|
+
* Field sections are encoded and decoded by `nish/net/qpack` and checked by
|
|
8
|
+
* `nish/net/http-fields`.
|
|
9
|
+
*
|
|
10
|
+
* It is sans-IO in the way the QUIC connection under it is: the carrier
|
|
11
|
+
* hands the `QuicConnection` its datagrams and takes the ones it sends, and
|
|
12
|
+
* this module reads and writes that connection's streams. `nish/net/http3-server`
|
|
13
|
+
* runs it over a UDP socket with ALPN `h3`.
|
|
14
|
+
*
|
|
15
|
+
* import { Http3Config, Http3Connection, H3_NEED_MORE, H3_ERROR, H3_REQUEST, H3_DATA, H3_END } from "nish/net/http3";
|
|
16
|
+
*
|
|
17
|
+
* const h3 = new Http3Connection(new Http3Config(), quic); // once per slot; restart() per connection
|
|
18
|
+
* // after quic.receive(...):
|
|
19
|
+
* let event: i32 = h3.next();
|
|
20
|
+
* while (event !== H3_NEED_MORE && event !== H3_ERROR) {
|
|
21
|
+
* // H3_REQUEST: h3.stream, h3.fields (method, path, get("accept"))
|
|
22
|
+
* // H3_DATA: h3.data[h3.dataStart .. h3.dataStart + h3.dataLength)
|
|
23
|
+
* // H3_END: the request is whole; respond
|
|
24
|
+
* event = h3.next();
|
|
25
|
+
* }
|
|
26
|
+
* h3.respond(stream, 200, names, values, false);
|
|
27
|
+
* h3.writeData(stream, body, 0, n, true); // answers how much it took
|
|
28
|
+
*
|
|
29
|
+
* **Events.** `next()` reads the streams the QUIC connection has news for and
|
|
30
|
+
* answers the first thing that means something to the program: `H3_REQUEST`
|
|
31
|
+
* (a request's field section, in `fields`), `H3_DATA` (a piece of its body, a
|
|
32
|
+
* window onto `data` valid until the next call), `H3_TRAILERS`, `H3_END` (the
|
|
33
|
+
* request ended, and its `content-length`, if any, matched), `H3_RESET` (the
|
|
34
|
+
* stream is gone: the client reset it or stopped it, or it broke a rule that
|
|
35
|
+
* resets it — `errorCode` says which, `resetByPeer` who), `H3_WRITABLE` (a
|
|
36
|
+
* stream whose write was held back can take more), `H3_GOAWAY` (the client is
|
|
37
|
+
* going away; `lastStreamId` is its push ID), and `H3_ERROR`, final: the
|
|
38
|
+
* connection broke a rule, it is closed with `errorCode`, and every later call
|
|
39
|
+
* answers it again. `H3_NEED_MORE` asks for more datagrams. A request's events
|
|
40
|
+
* come in order — REQUEST, DATA…, TRAILERS, END — and after `H3_RESET` its
|
|
41
|
+
* stream has no more.
|
|
42
|
+
*
|
|
43
|
+
* **Back-pressure** is QUIC's flow control. A body is read only as the
|
|
44
|
+
* program calls `next()`, `bodyChunk` bytes at most at a time, and QUIC gives
|
|
45
|
+
* the client more credit only as its receive buffer is read. `writeData`
|
|
46
|
+
* takes as much as the stream's send buffer has room for and answers how much;
|
|
47
|
+
* when it took less, `H3_WRITABLE` names the stream once acknowledgements free
|
|
48
|
+
* room. HEADERS and trailers go whole or not at all (`H3_AGAIN`).
|
|
49
|
+
*
|
|
50
|
+
* **ALPN.** A QUIC connection whose handshake chose anything but `h3` is
|
|
51
|
+
* closed with CRYPTO_ERROR no_application_protocol (0x0178) on the first
|
|
52
|
+
* `next()` after it connects, before any HTTP/3 byte (§3.1).
|
|
53
|
+
*
|
|
54
|
+
* **Streams** (§6). The connection opens its control stream, sends SETTINGS
|
|
55
|
+
* first on it (SETTINGS_QPACK_MAX_TABLE_CAPACITY 0, SETTINGS_QPACK_BLOCKED_STREAMS
|
|
56
|
+
* 0 and SETTINGS_MAX_FIELD_SECTION_SIZE), and opens its QPACK encoder and
|
|
57
|
+
* decoder streams, on which it sends nothing but their types. The client's
|
|
58
|
+
* encoder and decoder streams go to `nish/net/qpack`'s capacity-0 handling.
|
|
59
|
+
* A unidirectional stream of a type it does not know is asked to stop
|
|
60
|
+
* (H3_STREAM_CREATION_ERROR) and read to its end (§6.2).
|
|
61
|
+
*
|
|
62
|
+
* **What it refuses.** A connection error closes the QUIC connection with
|
|
63
|
+
* the code: a second control, encoder or decoder stream, or a push stream
|
|
64
|
+
* from a client (H3_STREAM_CREATION_ERROR, §6.2.1, §6.2.2); a control stream
|
|
65
|
+
* whose first frame is not SETTINGS (H3_MISSING_SETTINGS); the end, reset or
|
|
66
|
+
* STOP_SENDING of any critical stream, either side's (H3_CLOSED_CRITICAL_STREAM);
|
|
67
|
+
* a frame where it may not be — DATA or HEADERS on the control stream, a
|
|
68
|
+
* control frame on a request stream, a second SETTINGS, DATA before HEADERS,
|
|
69
|
+
* anything but the end after trailers, any PUSH_PROMISE from a client, and an
|
|
70
|
+
* HTTP/2 type HTTP/3 reserves (H3_FRAME_UNEXPECTED, §7.2.8); a frame cut off
|
|
71
|
+
* by its stream's end or a payload of the wrong length (H3_FRAME_ERROR, §7.1);
|
|
72
|
+
* a SETTINGS frame `nish/net/http3-frame` refuses (H3_SETTINGS_ERROR); a
|
|
73
|
+
* GOAWAY whose push ID rose, a MAX_PUSH_ID that fell, a CANCEL_PUSH past the
|
|
74
|
+
* push IDs allowed (H3_ID_ERROR, §5.2, §7.2.7, §7.2.3); a control frame past
|
|
75
|
+
* `H3_CONTROL_FRAME_MAX` (H3_EXCESSIVE_LOAD); and QPACK's three. A stream
|
|
76
|
+
* error resets one request, both ways, and the connection lives: a field
|
|
77
|
+
* section `nish/net/http-fields` refuses, and a body that disagrees with
|
|
78
|
+
* `content-length` (H3_MESSAGE_ERROR, §4.1.2); a request stream that ends
|
|
79
|
+
* before its HEADERS (H3_REQUEST_INCOMPLETE, §4.1.1); a request on a stream at
|
|
80
|
+
* or past the ID of the GOAWAY this side sent (H3_REQUEST_REJECTED, §5.2).
|
|
81
|
+
* A request whose field section is longer than `maxFieldSectionSize` — the
|
|
82
|
+
* frame, or the section once decoded — is answered with a 431, never reaches
|
|
83
|
+
* the program, and its stream is asked to stop with H3_NO_ERROR (§4.2.2,
|
|
84
|
+
* §4.1.2).
|
|
85
|
+
*
|
|
86
|
+
* **No server push.** It never sends PUSH_PROMISE; a client's MAX_PUSH_ID is
|
|
87
|
+
* allowed, checked and ignored.
|
|
88
|
+
*
|
|
89
|
+
* **Caps are start-up numbers**, all in `Http3Config`, and every buffer is
|
|
90
|
+
* made by the constructor: the stream state for every QUIC stream slot, the
|
|
91
|
+
* field section, the body chunk and the control frame. A field section's
|
|
92
|
+
* names and values are copied into arrays the connection keeps, which grow to
|
|
93
|
+
* the largest section seen and are then reused, so a warm connection
|
|
94
|
+
* allocates nothing per request (`docs/security/http3.md`, H3-2). A peer's
|
|
95
|
+
* counters saturate, and the lookup of a stream's state is the QUIC
|
|
96
|
+
* connection's own hash index (`QuicStreams.slotOf`), never a scan.
|
|
97
|
+
*
|
|
98
|
+
* **Extended CONNECT and WebTransport.** With `extendedConnect` the
|
|
99
|
+
* connection sends SETTINGS_ENABLE_CONNECT_PROTOCOL 1 and a request may carry
|
|
100
|
+
* `:protocol` (RFC 9220); without it `:protocol` is malformed,
|
|
101
|
+
* H3_MESSAGE_ERROR. `webtransportSessions` above 0 turns that on and also
|
|
102
|
+
* sends SETTINGS_H3_DATAGRAM 1, draft-02's SETTINGS_ENABLE_WEBTRANSPORT 1 and
|
|
103
|
+
* draft-07's SETTINGS_WEBTRANSPORT_MAX_SESSIONS, and `nish/net/webtransport`
|
|
104
|
+
* runs its sessions on top: a request then waits for the client's SETTINGS
|
|
105
|
+
* (draft-02 §3.1), and a stream that opens with WebTransport's unidirectional
|
|
106
|
+
* type (0x54) or bidirectional signal (0x41) is read as far as its session ID,
|
|
107
|
+
* held unread and named in `H3_STREAM` until the program calls
|
|
108
|
+
* `acceptStream` (its bytes then come as H3_DATA, H3_END, H3_RESET and
|
|
109
|
+
* H3_STOPPED, and `writeStream` writes them as they are) or `refuseStream`.
|
|
110
|
+
* `openStream` opens one of this side's; `writeDataWhole` writes a capsule;
|
|
111
|
+
* a GOAWAY still lets a session's new stream in, and `H3_DROPPED` names a
|
|
112
|
+
* request stream that ended before its request reached the program. The
|
|
113
|
+
* client's SETTINGS_ENABLE_CONNECT_PROTOCOL and SETTINGS_H3_DATAGRAM must be
|
|
114
|
+
* 0 or 1 (H3_SETTINGS_ERROR), a session ID that is no client-initiated
|
|
115
|
+
* bidirectional stream is H3_ID_ERROR, and the signal anywhere but a stream's
|
|
116
|
+
* start is H3_FRAME_ERROR. Holding every request, not only an extended
|
|
117
|
+
* CONNECT, until the client's SETTINGS keeps a decoded field section from
|
|
118
|
+
* having to be held per stream; it costs a request only when the client's
|
|
119
|
+
* control stream is lost or reordered.
|
|
120
|
+
*
|
|
121
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
122
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
123
|
+
*
|
|
124
|
+
* Written from RFC 9114 and RFC 9204, not ported from another implementation.
|
|
125
|
+
*/
|
|
126
|
+
import {
|
|
127
|
+
HTTP_FIELDS_OK,
|
|
128
|
+
HttpFields,
|
|
129
|
+
httpFieldBytes,
|
|
130
|
+
httpFieldSensitive,
|
|
131
|
+
httpFieldsCheckOutgoing,
|
|
132
|
+
} from "nish/net/http-fields"
|
|
133
|
+
import {
|
|
134
|
+
QPACK_FIELD_OVERHEAD,
|
|
135
|
+
QPACK_OK,
|
|
136
|
+
QPACK_SECTION_TOO_LARGE,
|
|
137
|
+
QpackDecoder,
|
|
138
|
+
QpackEncoder,
|
|
139
|
+
} from "nish/net/qpack"
|
|
140
|
+
import {
|
|
141
|
+
H3_CLOSED_CRITICAL_STREAM,
|
|
142
|
+
H3_EXCESSIVE_LOAD,
|
|
143
|
+
H3_FRAME_WEBTRANSPORT_STREAM,
|
|
144
|
+
H3_FRAME_CANCEL_PUSH,
|
|
145
|
+
H3_FRAME_DATA,
|
|
146
|
+
H3_FRAME_ERROR,
|
|
147
|
+
H3_FRAME_GOAWAY,
|
|
148
|
+
H3_FRAME_HEADERS,
|
|
149
|
+
H3_FRAME_MAX_PUSH_ID,
|
|
150
|
+
H3_FRAME_PUSH_PROMISE,
|
|
151
|
+
H3_FRAME_SETTINGS,
|
|
152
|
+
H3_FRAME_UNEXPECTED,
|
|
153
|
+
H3_ID_ERROR,
|
|
154
|
+
H3_INTERNAL_ERROR,
|
|
155
|
+
H3_MESSAGE_ERROR,
|
|
156
|
+
H3_MISSING_SETTINGS,
|
|
157
|
+
H3_NO_ERROR,
|
|
158
|
+
H3_REQUEST_CANCELLED,
|
|
159
|
+
H3_REQUEST_INCOMPLETE,
|
|
160
|
+
H3_REQUEST_REJECTED,
|
|
161
|
+
H3_SETTINGS_ENABLE_CONNECT_PROTOCOL,
|
|
162
|
+
H3_SETTINGS_ENABLE_WEBTRANSPORT,
|
|
163
|
+
H3_SETTINGS_ERROR,
|
|
164
|
+
H3_SETTINGS_H3_DATAGRAM,
|
|
165
|
+
H3_SETTINGS_MAX_FIELD_SECTION_SIZE,
|
|
166
|
+
H3_SETTINGS_OK,
|
|
167
|
+
H3_SETTINGS_QPACK_BLOCKED_STREAMS,
|
|
168
|
+
H3_SETTINGS_QPACK_MAX_TABLE_CAPACITY,
|
|
169
|
+
H3_SETTINGS_WEBTRANSPORT_MAX_SESSIONS,
|
|
170
|
+
H3_STREAM_CONTROL,
|
|
171
|
+
H3_STREAM_CREATION_ERROR,
|
|
172
|
+
H3_STREAM_PUSH,
|
|
173
|
+
H3_STREAM_QPACK_DECODER,
|
|
174
|
+
H3_STREAM_QPACK_ENCODER,
|
|
175
|
+
H3_STREAM_WEBTRANSPORT,
|
|
176
|
+
H3_WT_SESSION_GONE,
|
|
177
|
+
Http3FrameHeader,
|
|
178
|
+
Http3Settings,
|
|
179
|
+
h3CheckWindow,
|
|
180
|
+
h3Count,
|
|
181
|
+
h3FrameHeaderSize,
|
|
182
|
+
h3PutFrameHeader,
|
|
183
|
+
h3PutIdFrame,
|
|
184
|
+
h3PutSettings,
|
|
185
|
+
h3PutVarint,
|
|
186
|
+
h3ReadFrameHeader,
|
|
187
|
+
h3ReadIdPayload,
|
|
188
|
+
h3ReadSettings,
|
|
189
|
+
h3ReadVarint,
|
|
190
|
+
h3ReservedHttp2Frame,
|
|
191
|
+
h3VarintLength,
|
|
192
|
+
} from "nish/net/http3-frame"
|
|
193
|
+
import { QUIC_STATE_CONNECTED, QuicConnection } from "nish/net/quic"
|
|
194
|
+
import { QUIC_STREAM_END, QUIC_STREAM_ERR_RESET, QuicStream } from "nish/net/quic-stream"
|
|
195
|
+
import { quicVarintPut } from "nish/net/quic-packet"
|
|
196
|
+
|
|
197
|
+
/** The ALPN protocol identifier of HTTP/3 (RFC 9114 §3.1). */
|
|
198
|
+
export const H3_ALPN: string = "h3"
|
|
199
|
+
|
|
200
|
+
/** `next` has nothing more until more datagrams arrive. */
|
|
201
|
+
export const H3_NEED_MORE: i32 = 0
|
|
202
|
+
/** A request's field section: `stream`, `fields`. */
|
|
203
|
+
export const H3_REQUEST: i32 = 1
|
|
204
|
+
/** Request body bytes: `stream`, `data[dataStart .. dataStart + dataLength)`. */
|
|
205
|
+
export const H3_DATA: i32 = 2
|
|
206
|
+
/** A trailer section: `stream`, `fields`. */
|
|
207
|
+
export const H3_TRAILERS: i32 = 3
|
|
208
|
+
/** The request ended whole: `stream`. */
|
|
209
|
+
export const H3_END: i32 = 4
|
|
210
|
+
/** The stream is gone: `stream`, `errorCode`, `resetByPeer`. */
|
|
211
|
+
export const H3_RESET: i32 = 5
|
|
212
|
+
/** A stream whose write was held back can take more: `stream`. */
|
|
213
|
+
export const H3_WRITABLE: i32 = 6
|
|
214
|
+
/** The client sent GOAWAY: `lastStreamId`, its push ID. */
|
|
215
|
+
export const H3_GOAWAY: i32 = 7
|
|
216
|
+
/** The connection failed with `errorCode` and is closed. Final. */
|
|
217
|
+
export const H3_ERROR: i32 = 8
|
|
218
|
+
/**
|
|
219
|
+
* A WebTransport stream arrived (only with `webtransportSessions` on):
|
|
220
|
+
* `stream`, and `session`, the session ID its first bytes named. It is held,
|
|
221
|
+
* unread, until the program calls `acceptStream` or `refuseStream`.
|
|
222
|
+
*/
|
|
223
|
+
export const H3_STREAM: i32 = 9
|
|
224
|
+
/** The client sent STOP_SENDING for a WebTransport stream this side writes, and QUIC reset it: `stream`, `errorCode`. */
|
|
225
|
+
export const H3_STOPPED: i32 = 10
|
|
226
|
+
/**
|
|
227
|
+
* A request stream ended before its request reached the program (only with
|
|
228
|
+
* `webtransportSessions` on): `stream`. Nothing is owed the program; a
|
|
229
|
+
* WebTransport layer lets go of the streams that waited for it as a session.
|
|
230
|
+
*/
|
|
231
|
+
export const H3_DROPPED: i32 = 11
|
|
232
|
+
|
|
233
|
+
/** A write that can take nothing yet: Linux's EAGAIN, as `nish:net` spells it. */
|
|
234
|
+
export const H3_AGAIN: i32 = -11
|
|
235
|
+
/** A write of fields or a status the program may not send, or out of order: Linux's EINVAL. */
|
|
236
|
+
export const H3_INVALID: i32 = -22
|
|
237
|
+
/** A write to a stream that is not an open request: Linux's EPIPE. */
|
|
238
|
+
export const H3_CLOSED: i32 = -32
|
|
239
|
+
/** A field section past what the client takes, or past a stream's buffer: Linux's EMSGSIZE. */
|
|
240
|
+
export const H3_TOO_LARGE: i32 = -90
|
|
241
|
+
|
|
242
|
+
/** CRYPTO_ERROR with TLS's no_application_protocol alert (RFC 9001 §4.8): a handshake that did not choose `h3`. */
|
|
243
|
+
export const H3_NO_APPLICATION_PROTOCOL: i64 = 0x0178
|
|
244
|
+
|
|
245
|
+
/** The largest control-stream frame read whole: SETTINGS, GOAWAY, MAX_PUSH_ID, CANCEL_PUSH. */
|
|
246
|
+
export const H3_CONTROL_FRAME_MAX: i32 = 1024
|
|
247
|
+
|
|
248
|
+
/** The most WebTransport sessions a configuration may advertise. */
|
|
249
|
+
export const H3_MAX_SESSIONS: i32 = 256
|
|
250
|
+
|
|
251
|
+
/** The request streams' phases, and the unidirectional streams'. */
|
|
252
|
+
const H3_PHASE_HEADERS: i32 = 0
|
|
253
|
+
const H3_PHASE_BODY: i32 = 1
|
|
254
|
+
const H3_PHASE_TRAILED: i32 = 2
|
|
255
|
+
const H3_PHASE_SKIP: i32 = 3
|
|
256
|
+
const H3_PHASE_DISCARD: i32 = 4
|
|
257
|
+
const H3_PHASE_DONE: i32 = 5
|
|
258
|
+
const H3_PHASE_UNI_TYPE: i32 = 6
|
|
259
|
+
const H3_PHASE_CONTROL: i32 = 7
|
|
260
|
+
const H3_PHASE_ENCODER: i32 = 8
|
|
261
|
+
const H3_PHASE_DECODER: i32 = 9
|
|
262
|
+
/** A WebTransport unidirectional stream whose session ID is being read, one held for the program, and one read raw. */
|
|
263
|
+
const H3_PHASE_WT_SESSION: i32 = 10
|
|
264
|
+
const H3_PHASE_WT_HELD: i32 = 11
|
|
265
|
+
const H3_PHASE_WT: i32 = 12
|
|
266
|
+
|
|
267
|
+
/** A stream slot's flags. */
|
|
268
|
+
const H3_FLAG_DELIVERED: i32 = 1
|
|
269
|
+
const H3_FLAG_HEADERS_SENT: i32 = 2
|
|
270
|
+
const H3_FLAG_SEND_DONE: i32 = 4
|
|
271
|
+
const H3_FLAG_RECV_DONE: i32 = 8
|
|
272
|
+
const H3_FLAG_BLOCKED: i32 = 32
|
|
273
|
+
const H3_FLAG_COUNTED: i32 = 64
|
|
274
|
+
/** On the ring of streams `next` steps again before asking QUIC, or waiting for the client's SETTINGS. */
|
|
275
|
+
const H3_FLAG_AGAIN: i32 = 128
|
|
276
|
+
const H3_FLAG_WAITING: i32 = 16
|
|
277
|
+
/** A WebTransport stream: its bytes are the session's, read and written raw. */
|
|
278
|
+
const H3_FLAG_WT: i32 = 256
|
|
279
|
+
/** A request stream past this side's GOAWAY, refused unless its first bytes make it a WebTransport stream. */
|
|
280
|
+
const H3_FLAG_LATE: i32 = 512
|
|
281
|
+
|
|
282
|
+
/** The connection's states. */
|
|
283
|
+
const H3_STATE_OPEN: i32 = 0
|
|
284
|
+
const H3_STATE_FAILED: i32 = 1
|
|
285
|
+
|
|
286
|
+
/** What `requestFrame` answers when it read a frame header and the stream has more to read. */
|
|
287
|
+
const H3_CONTINUE: i32 = -1
|
|
288
|
+
|
|
289
|
+
/** A frame type no frame has, for a payload being skipped. */
|
|
290
|
+
const H3_SKIP_TYPE: i64 = -1
|
|
291
|
+
|
|
292
|
+
/** A reserved frame type (0x1f * 0 + 0x21) the connection pads a stream's send buffer with to be told when it drains. */
|
|
293
|
+
const H3_FILLER_TYPE: i32 = 0x21
|
|
294
|
+
|
|
295
|
+
/** Typed constants, since a bare literal is an `f64` under `--number-mode f64`. */
|
|
296
|
+
const H3_ZERO: i32 = 0
|
|
297
|
+
const H3_ONE: i32 = 1
|
|
298
|
+
const H3_NONE: i64 = -1
|
|
299
|
+
const H3_NONE32: i32 = -1
|
|
300
|
+
const H3_STATUS_TOO_LARGE: i32 = 431
|
|
301
|
+
const H3_DIGIT_ZERO: i32 = 48
|
|
302
|
+
|
|
303
|
+
/** The largest body chunk or DATA frame a configuration may ask for. */
|
|
304
|
+
const H3_MAX_CHUNK: i32 = 1048576
|
|
305
|
+
|
|
306
|
+
/** The size classes of a field's copy: up to 2^23 bytes, past any stream buffer QUIC allows. */
|
|
307
|
+
const H3_FIELD_CLASSES: i32 = 24
|
|
308
|
+
|
|
309
|
+
/** The bytes a slot keeps for a frame header being read or written. */
|
|
310
|
+
const H3_HEAD: i32 = 16
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The connection's caps, fixed when it is made. `maxFieldSectionSize` is
|
|
314
|
+
* advertised in SETTINGS and is the largest HEADERS frame read and the
|
|
315
|
+
* largest section decoded; `bodyChunk` the most body handed to the program at
|
|
316
|
+
* once; `writeChunk` the largest DATA frame written.
|
|
317
|
+
*/
|
|
318
|
+
export class Http3Config {
|
|
319
|
+
/** SETTINGS_MAX_FIELD_SECTION_SIZE (§4.2.2): past it a request is answered with a 431. */
|
|
320
|
+
maxFieldSectionSize: i32 = 16384
|
|
321
|
+
/** The body buffer: the largest `H3_DATA` window. */
|
|
322
|
+
bodyChunk: i32 = 16384
|
|
323
|
+
/** The largest DATA frame `writeData` starts. */
|
|
324
|
+
writeChunk: i32 = 16384
|
|
325
|
+
/**
|
|
326
|
+
* The WebTransport sessions this side takes at once
|
|
327
|
+
* (`nish/net/webtransport`), 0 for none. Above 0 it turns on extended
|
|
328
|
+
* CONNECT and sends SETTINGS_H3_DATAGRAM, SETTINGS_ENABLE_WEBTRANSPORT and
|
|
329
|
+
* SETTINGS_WEBTRANSPORT_MAX_SESSIONS, and the QUIC connection must take
|
|
330
|
+
* DATAGRAM frames; at 0 it must not (RFC 9297 §2.1.1).
|
|
331
|
+
*/
|
|
332
|
+
webtransportSessions: i32 = 0
|
|
333
|
+
/**
|
|
334
|
+
* Whether `:protocol` is accepted (RFC 9220's extended CONNECT), and
|
|
335
|
+
* SETTINGS_ENABLE_CONNECT_PROTOCOL sent. WebTransport turns it on.
|
|
336
|
+
*/
|
|
337
|
+
extendedConnect: boolean = false
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Panics unless `value` lies in `[low, high]`: a cap out of range is the program's mistake. */
|
|
341
|
+
const http3CheckCap = (what: string, value: i32, low: i32, high: i32): void => {
|
|
342
|
+
if (value < low || value > high) {
|
|
343
|
+
panic(`Http3Connection: ${what} of ${value}, outside ${low} to ${high}`)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Empties `bytes`, keeping its storage for the next fill. */
|
|
348
|
+
const http3Empty = (bytes: u8[]): void => {
|
|
349
|
+
while (toI32(bytes.length) > 0) {
|
|
350
|
+
bytes.pop()
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Empties `list`, keeping its storage for the next fill. */
|
|
355
|
+
const http3EmptyList = (list: u8[][]): void => {
|
|
356
|
+
while (toI32(list.length) > 0) {
|
|
357
|
+
list.pop()
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Whether `id` is a stream the client opened carrying data both ways: a request stream. */
|
|
362
|
+
const http3IsRequest = (id: i64): boolean => (id & 3) === 0
|
|
363
|
+
|
|
364
|
+
/** The smaller of two `i64`s. */
|
|
365
|
+
const http3Min = (a: i64, b: i64): i64 => (a < b ? a : b)
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* One HTTP/3 connection, server side, over `quic`. The fields after the
|
|
369
|
+
* buffers are the last event's; the module comment says which event sets
|
|
370
|
+
* which.
|
|
371
|
+
*/
|
|
372
|
+
export class Http3Connection {
|
|
373
|
+
// Pointers and 64-bit fields first, then 32-bit ones, then flags: the
|
|
374
|
+
// order the layout packs without padding.
|
|
375
|
+
config: Http3Config
|
|
376
|
+
quic: QuicConnection
|
|
377
|
+
decoder: QpackDecoder
|
|
378
|
+
encoder: QpackEncoder
|
|
379
|
+
/** The last request's or trailers' fields, valid until the next of either. */
|
|
380
|
+
fields: HttpFields
|
|
381
|
+
/** The client's SETTINGS. */
|
|
382
|
+
peer: Http3Settings
|
|
383
|
+
header: Http3FrameHeader
|
|
384
|
+
/** H3_DATA's bytes, and where a payload being skipped is read to. */
|
|
385
|
+
data: u8[]
|
|
386
|
+
/** A HEADERS payload read whole, for the decoder. */
|
|
387
|
+
section: u8[]
|
|
388
|
+
/** A control frame's payload, read whole. */
|
|
389
|
+
control: u8[]
|
|
390
|
+
/** Frame headers, SETTINGS and GOAWAY on their way out. */
|
|
391
|
+
scratch: u8[]
|
|
392
|
+
/** Zeros, the payload of a filler frame. */
|
|
393
|
+
zeros: u8[]
|
|
394
|
+
/** A response's field section, encoded, emptied and reused. */
|
|
395
|
+
encoded: u8[]
|
|
396
|
+
/** Copies of the decoded names and values, in arrays by size class (`copyOf`), and the lists handed to `fields`. */
|
|
397
|
+
pools: u8[][][]
|
|
398
|
+
poolUsed: i32[]
|
|
399
|
+
nameList: u8[][]
|
|
400
|
+
valueList: u8[][]
|
|
401
|
+
statusName: u8[]
|
|
402
|
+
statusValue: u8[]
|
|
403
|
+
/** The SETTINGS this side sends. */
|
|
404
|
+
settingIds: i64[]
|
|
405
|
+
settingValues: i64[]
|
|
406
|
+
/** Per QUIC stream slot: the stream the state is for, or -1. */
|
|
407
|
+
slotId: i64[]
|
|
408
|
+
/** The frame being read: its type, and what is left of its payload (-1 between frames). */
|
|
409
|
+
frameType: i64[]
|
|
410
|
+
frameLeft: i64[]
|
|
411
|
+
/** DATA bytes received on the request, and its `content-length` or -1. */
|
|
412
|
+
dataTotal: i64[]
|
|
413
|
+
contentLength: i64[]
|
|
414
|
+
/** Bytes still owed on the stream: a filler's payload, and the DATA frame's payload the program is writing. */
|
|
415
|
+
fillLeft: i64[]
|
|
416
|
+
dataLeft: i64[]
|
|
417
|
+
phase: i32[]
|
|
418
|
+
flags: i32[]
|
|
419
|
+
/** A frame header (or a stream type) being read, `headLen[k]` bytes of `head[k * 16 ..]`. */
|
|
420
|
+
headLen: i32[]
|
|
421
|
+
head: u8[]
|
|
422
|
+
/** Frame-header bytes owed before anything else, `pendLen[k]` of `pend[k * 16 + pendAt[k] ..]`. */
|
|
423
|
+
pendAt: i32[]
|
|
424
|
+
pendLen: i32[]
|
|
425
|
+
pend: u8[]
|
|
426
|
+
/**
|
|
427
|
+
* Streams `next` steps again before asking QUIC for news, a ring of
|
|
428
|
+
* `againCount` from `againHead`: WebTransport streams the program let go,
|
|
429
|
+
* and requests that waited for the client's SETTINGS. And those waiting,
|
|
430
|
+
* `waitCount` of them. A stream is on each at most once (its flags say),
|
|
431
|
+
* so neither needs more room than there are stream slots.
|
|
432
|
+
*/
|
|
433
|
+
again: i64[]
|
|
434
|
+
waiting: i64[]
|
|
435
|
+
/** This side's streams, and the client's critical ones; -1 until there. */
|
|
436
|
+
ownControl: i64 = -1
|
|
437
|
+
ownEncoder: i64 = -1
|
|
438
|
+
ownDecoder: i64 = -1
|
|
439
|
+
peerControl: i64 = -1
|
|
440
|
+
peerEncoder: i64 = -1
|
|
441
|
+
peerDecoder: i64 = -1
|
|
442
|
+
/** The stream `next` is reading, or -1. */
|
|
443
|
+
active: i64 = -1
|
|
444
|
+
/** The ID this side's GOAWAY named, or -1; the client's last GOAWAY push ID, and its MAX_PUSH_ID. */
|
|
445
|
+
goawayId: i64 = -1
|
|
446
|
+
peerGoaway: i64 = -1
|
|
447
|
+
maxPushId: i64 = -1
|
|
448
|
+
/** The last event's stream. */
|
|
449
|
+
stream: i64 = -1
|
|
450
|
+
/** H3_RESET's and H3_ERROR's code. */
|
|
451
|
+
errorCode: i64 = 0
|
|
452
|
+
/** H3_GOAWAY's push ID. */
|
|
453
|
+
lastStreamId: i64 = 0
|
|
454
|
+
/** H3_STREAM's session ID. */
|
|
455
|
+
session: i64 = -1
|
|
456
|
+
/** The QUIC stream slot of the last event's stream: still right when the read that made the event freed it. */
|
|
457
|
+
slot: i32 = -1
|
|
458
|
+
/** H3_DATA's window onto `data`. */
|
|
459
|
+
dataStart: i32 = 0
|
|
460
|
+
dataLength: i32 = 0
|
|
461
|
+
/** How much of `control` is filled. */
|
|
462
|
+
controlFill: i32 = 0
|
|
463
|
+
state: i32 = 0
|
|
464
|
+
/** Requests open: taken and not yet finished both ways. */
|
|
465
|
+
live: i32 = 0
|
|
466
|
+
/** Counters for a log or a test, each saturating: requests refused after GOAWAY, answered 431, and reset for a rule. */
|
|
467
|
+
rejected: i32 = 0
|
|
468
|
+
tooLarge: i32 = 0
|
|
469
|
+
streamErrors: i32 = 0
|
|
470
|
+
againHead: i32 = 0
|
|
471
|
+
againCount: i32 = 0
|
|
472
|
+
waitCount: i32 = 0
|
|
473
|
+
/** How many times `restart` ran: a layer above that keeps state per connection compares it to know when to forget its own. */
|
|
474
|
+
generation: i32 = 0
|
|
475
|
+
/** Whether the client's SETTINGS have arrived. */
|
|
476
|
+
settingsSeen: boolean = false
|
|
477
|
+
/** Whether H3_RESET came from the client. */
|
|
478
|
+
resetByPeer: boolean = false
|
|
479
|
+
/** Whether extended CONNECT is on, and WebTransport. */
|
|
480
|
+
connect: boolean = false
|
|
481
|
+
webtransport: boolean = false
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* A connection under `config` over `quic`, whose configuration must let a
|
|
485
|
+
* HEADERS frame of `maxFieldSectionSize` fit half a stream's buffer, and
|
|
486
|
+
* one on every request stream fit half the connection's window, and let
|
|
487
|
+
* the client open the three unidirectional streams it needs and this side
|
|
488
|
+
* open its own three (§6.2). A cap out of range panics.
|
|
489
|
+
*/
|
|
490
|
+
constructor(config: Http3Config, quic: QuicConnection) {
|
|
491
|
+
const buffer: i32 = quic.streams.bufferSize
|
|
492
|
+
// A HEADERS frame is read once all of it is in the stream's buffer, and QUIC
|
|
493
|
+
// gives more credit only once half a buffer has been read, so the frame
|
|
494
|
+
// may be at most half a buffer, its header aside, or it could stall.
|
|
495
|
+
http3CheckCap("maxFieldSectionSize", config.maxFieldSectionSize, 1, buffer / 2 - H3_HEAD)
|
|
496
|
+
// The connection's credit too: every request stream may hold a HEADERS
|
|
497
|
+
// frame not yet whole, and QUIC raises MAX_DATA once half the window is read.
|
|
498
|
+
const held: i64 = quic.streams.maxBidi * toI64(config.maxFieldSectionSize + H3_HEAD) * toI64(2)
|
|
499
|
+
if (quic.streams.windowData < held) {
|
|
500
|
+
panic(
|
|
501
|
+
`Http3Connection: a connection window of ${quic.streams.windowData}, under the ${held} that ${quic.streams.maxBidi} request streams' field sections need`
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
http3CheckCap("bodyChunk", config.bodyChunk, 1, H3_MAX_CHUNK)
|
|
505
|
+
http3CheckCap("writeChunk", config.writeChunk, 1, H3_MAX_CHUNK)
|
|
506
|
+
http3CheckCap("the QUIC unidirectional stream limit", toI32(quic.streams.maxUni), 3, 1024)
|
|
507
|
+
http3CheckCap("the QUIC local stream limit", toI32(quic.streams.localStreams), 3, 1024)
|
|
508
|
+
http3CheckCap("webtransportSessions", config.webtransportSessions, 0, H3_MAX_SESSIONS)
|
|
509
|
+
// RFC 9297 §2.1.1: QUIC datagrams carry HTTP datagrams, which only WebTransport uses here.
|
|
510
|
+
const datagrams: boolean = quic.datagramsIn.capacity() > 0
|
|
511
|
+
if (config.webtransportSessions > 0 && !datagrams) {
|
|
512
|
+
panic("Http3Connection: WebTransport needs QUIC datagrams, and the QUIC maxDatagramFrameSize is 0")
|
|
513
|
+
}
|
|
514
|
+
if (config.webtransportSessions === 0 && datagrams) {
|
|
515
|
+
panic("Http3Connection: QUIC datagrams advertised with WebTransport off (webtransportSessions 0)")
|
|
516
|
+
}
|
|
517
|
+
this.webtransport = config.webtransportSessions > 0
|
|
518
|
+
this.connect = config.extendedConnect || this.webtransport
|
|
519
|
+
this.config = config
|
|
520
|
+
this.quic = quic
|
|
521
|
+
this.decoder = new QpackDecoder(config.maxFieldSectionSize)
|
|
522
|
+
this.encoder = new QpackEncoder()
|
|
523
|
+
this.fields = new HttpFields()
|
|
524
|
+
this.peer = new Http3Settings()
|
|
525
|
+
this.header = new Http3FrameHeader()
|
|
526
|
+
this.data = new Array<u8>(config.bodyChunk)
|
|
527
|
+
this.section = new Array<u8>(config.maxFieldSectionSize)
|
|
528
|
+
this.control = new Array<u8>(H3_CONTROL_FRAME_MAX)
|
|
529
|
+
this.scratch = new Array<u8>(64)
|
|
530
|
+
this.zeros = new Array<u8>(256)
|
|
531
|
+
this.encoded = []
|
|
532
|
+
this.pools = []
|
|
533
|
+
for (let j: i32 = 0; j < H3_FIELD_CLASSES; j++) {
|
|
534
|
+
const pool: u8[][] = []
|
|
535
|
+
this.pools.push(pool)
|
|
536
|
+
}
|
|
537
|
+
this.poolUsed = new Array<i32>(H3_FIELD_CLASSES)
|
|
538
|
+
this.nameList = []
|
|
539
|
+
this.valueList = []
|
|
540
|
+
this.statusName = httpFieldBytes(":status")
|
|
541
|
+
this.statusValue = [48, 48, 48]
|
|
542
|
+
this.settingIds = [
|
|
543
|
+
H3_SETTINGS_QPACK_MAX_TABLE_CAPACITY,
|
|
544
|
+
H3_SETTINGS_QPACK_BLOCKED_STREAMS,
|
|
545
|
+
H3_SETTINGS_MAX_FIELD_SECTION_SIZE,
|
|
546
|
+
]
|
|
547
|
+
this.settingValues = [toI64(0), toI64(0), toI64(config.maxFieldSectionSize)]
|
|
548
|
+
if (this.connect) {
|
|
549
|
+
this.settingIds.push(H3_SETTINGS_ENABLE_CONNECT_PROTOCOL)
|
|
550
|
+
this.settingValues.push(toI64(1))
|
|
551
|
+
}
|
|
552
|
+
if (this.webtransport) {
|
|
553
|
+
this.settingIds.push(H3_SETTINGS_H3_DATAGRAM)
|
|
554
|
+
this.settingValues.push(toI64(1))
|
|
555
|
+
this.settingIds.push(H3_SETTINGS_ENABLE_WEBTRANSPORT)
|
|
556
|
+
this.settingValues.push(toI64(1))
|
|
557
|
+
this.settingIds.push(H3_SETTINGS_WEBTRANSPORT_MAX_SESSIONS)
|
|
558
|
+
this.settingValues.push(toI64(config.webtransportSessions))
|
|
559
|
+
}
|
|
560
|
+
const n: i32 = toI32(quic.streams.slots.length)
|
|
561
|
+
this.slotId = new Array<i64>(n)
|
|
562
|
+
this.frameType = new Array<i64>(n)
|
|
563
|
+
this.frameLeft = new Array<i64>(n)
|
|
564
|
+
this.dataTotal = new Array<i64>(n)
|
|
565
|
+
this.contentLength = new Array<i64>(n)
|
|
566
|
+
this.fillLeft = new Array<i64>(n)
|
|
567
|
+
this.dataLeft = new Array<i64>(n)
|
|
568
|
+
this.phase = new Array<i32>(n)
|
|
569
|
+
this.flags = new Array<i32>(n)
|
|
570
|
+
this.headLen = new Array<i32>(n)
|
|
571
|
+
this.head = new Array<u8>(n * H3_HEAD)
|
|
572
|
+
this.pendAt = new Array<i32>(n)
|
|
573
|
+
this.pendLen = new Array<i32>(n)
|
|
574
|
+
this.pend = new Array<u8>(n * H3_HEAD)
|
|
575
|
+
this.again = new Array<i64>(n > 0 ? n : H3_ONE)
|
|
576
|
+
this.waiting = new Array<i64>(n > 0 ? n : H3_ONE)
|
|
577
|
+
this.restart()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Puts the connection back as the constructor left it, for the QUIC
|
|
582
|
+
* connection's next peer (call it after `quic.reset`): no stream known,
|
|
583
|
+
* QPACK's state and the client's SETTINGS forgotten. It allocates nothing.
|
|
584
|
+
*/
|
|
585
|
+
restart(): void {
|
|
586
|
+
this.slotId.fill(H3_NONE)
|
|
587
|
+
this.flags.fill(H3_ZERO)
|
|
588
|
+
this.headLen.fill(H3_ZERO)
|
|
589
|
+
this.pendLen.fill(H3_ZERO)
|
|
590
|
+
this.fillLeft.fill(toI64(0))
|
|
591
|
+
this.dataLeft.fill(toI64(0))
|
|
592
|
+
this.decoder.failed = QPACK_OK
|
|
593
|
+
this.decoder.reason = 0
|
|
594
|
+
this.decoder.capacityInstructions = 0
|
|
595
|
+
this.encoder.failed = QPACK_OK
|
|
596
|
+
this.encoder.reason = 0
|
|
597
|
+
this.encoder.cancellations = 0
|
|
598
|
+
this.encoder.lastCancelled = -1
|
|
599
|
+
this.encoder.pendingId = 0
|
|
600
|
+
this.encoder.pendingShift = -1
|
|
601
|
+
this.fields.clear()
|
|
602
|
+
this.peer.reset()
|
|
603
|
+
this.ownControl = -1
|
|
604
|
+
this.ownEncoder = -1
|
|
605
|
+
this.ownDecoder = -1
|
|
606
|
+
this.peerControl = -1
|
|
607
|
+
this.peerEncoder = -1
|
|
608
|
+
this.peerDecoder = -1
|
|
609
|
+
this.active = -1
|
|
610
|
+
this.goawayId = -1
|
|
611
|
+
this.peerGoaway = -1
|
|
612
|
+
this.maxPushId = -1
|
|
613
|
+
this.stream = -1
|
|
614
|
+
this.errorCode = 0
|
|
615
|
+
this.lastStreamId = 0
|
|
616
|
+
this.session = -1
|
|
617
|
+
this.slot = -1
|
|
618
|
+
this.againHead = 0
|
|
619
|
+
this.againCount = 0
|
|
620
|
+
this.waitCount = 0
|
|
621
|
+
this.generation = this.generation < 2147483647 ? this.generation + 1 : H3_ONE
|
|
622
|
+
this.dataStart = 0
|
|
623
|
+
this.dataLength = 0
|
|
624
|
+
this.controlFill = 0
|
|
625
|
+
this.state = H3_STATE_OPEN
|
|
626
|
+
this.live = 0
|
|
627
|
+
this.rejected = 0
|
|
628
|
+
this.tooLarge = 0
|
|
629
|
+
this.streamErrors = 0
|
|
630
|
+
this.settingsSeen = false
|
|
631
|
+
this.resetByPeer = false
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ---- The connection -----------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
/** Closes the connection with `code` (a connection error) and answers H3_ERROR. */
|
|
637
|
+
fail(code: i64): i32 {
|
|
638
|
+
if (this.state !== H3_STATE_FAILED) {
|
|
639
|
+
this.state = H3_STATE_FAILED
|
|
640
|
+
this.errorCode = code
|
|
641
|
+
this.quic.close(code)
|
|
642
|
+
}
|
|
643
|
+
return H3_ERROR
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Whether this side sent GOAWAY and every request it took has finished
|
|
648
|
+
* both ways, every byte of each response acknowledged: the carrier may
|
|
649
|
+
* close the connection.
|
|
650
|
+
*/
|
|
651
|
+
isDone(): boolean {
|
|
652
|
+
const streams = this.quic.streams
|
|
653
|
+
return this.goawayId >= 0 && this.live === 0 && streams.peerBidiClosed >= streams.peerBidiOpened
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Writes `buf[0 .. n)` to this side's new stream `id` whole, as its first bytes; answers whether it took them. */
|
|
657
|
+
writeNew(id: i64, buf: u8[], n: i32): boolean {
|
|
658
|
+
return n > 0 && this.quic.streamWrite(id, buf, H3_ZERO, n, false) === n
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Opens this side's control stream with its type and SETTINGS, and its
|
|
663
|
+
* QPACK encoder and decoder streams with their types (§6.2.1, RFC 9204
|
|
664
|
+
* §4.2). A stream the client's MAX_STREAMS does not allow yet is opened on
|
|
665
|
+
* a later call.
|
|
666
|
+
*/
|
|
667
|
+
start(): void {
|
|
668
|
+
const end: i32 = toI32(this.scratch.length)
|
|
669
|
+
if (this.ownControl < 0) {
|
|
670
|
+
const id: i64 = this.quic.openStream(false)
|
|
671
|
+
if (id < 0) {
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
this.ownControl = id
|
|
675
|
+
const p: i32 = h3PutVarint(this.scratch, H3_ZERO, end, H3_STREAM_CONTROL)
|
|
676
|
+
const q: i32 = h3PutSettings(this.scratch, p, end, this.settingIds, this.settingValues)
|
|
677
|
+
if (!this.writeNew(id, this.scratch, q)) {
|
|
678
|
+
this.fail(H3_INTERNAL_ERROR)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (this.ownEncoder < 0) {
|
|
683
|
+
const id: i64 = this.quic.openStream(false)
|
|
684
|
+
if (id < 0) {
|
|
685
|
+
return
|
|
686
|
+
}
|
|
687
|
+
this.ownEncoder = id
|
|
688
|
+
if (
|
|
689
|
+
!this.writeNew(id, this.scratch, h3PutVarint(this.scratch, H3_ZERO, end, H3_STREAM_QPACK_ENCODER))
|
|
690
|
+
) {
|
|
691
|
+
this.fail(H3_INTERNAL_ERROR)
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (this.ownDecoder < 0) {
|
|
696
|
+
const id: i64 = this.quic.openStream(false)
|
|
697
|
+
if (id < 0) {
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
this.ownDecoder = id
|
|
701
|
+
if (
|
|
702
|
+
!this.writeNew(id, this.scratch, h3PutVarint(this.scratch, H3_ZERO, end, H3_STREAM_QPACK_DECODER))
|
|
703
|
+
) {
|
|
704
|
+
this.fail(H3_INTERNAL_ERROR)
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Reads what the QUIC connection has for the program and answers the next
|
|
712
|
+
* event (see the module comment). Call it after every datagram the QUIC
|
|
713
|
+
* connection takes, until it answers H3_NEED_MORE or H3_ERROR.
|
|
714
|
+
*/
|
|
715
|
+
next(): i32 {
|
|
716
|
+
if (this.state === H3_STATE_FAILED) {
|
|
717
|
+
return H3_ERROR
|
|
718
|
+
}
|
|
719
|
+
if (this.quic.closed()) {
|
|
720
|
+
this.state = H3_STATE_FAILED
|
|
721
|
+
this.errorCode = this.quic.error
|
|
722
|
+
return H3_ERROR
|
|
723
|
+
}
|
|
724
|
+
if (this.quic.state !== QUIC_STATE_CONNECTED) {
|
|
725
|
+
return H3_NEED_MORE
|
|
726
|
+
}
|
|
727
|
+
if (this.ownDecoder < 0) {
|
|
728
|
+
// RFC 9114 §3.1: HTTP/3 only where the handshake chose it.
|
|
729
|
+
if (this.quic.alpn !== H3_ALPN) {
|
|
730
|
+
this.quic.fail(H3_NO_APPLICATION_PROTOCOL, toI64(0))
|
|
731
|
+
return this.fail(H3_NO_APPLICATION_PROTOCOL)
|
|
732
|
+
}
|
|
733
|
+
this.start()
|
|
734
|
+
}
|
|
735
|
+
while (this.state !== H3_STATE_FAILED) {
|
|
736
|
+
if (this.active < 0) {
|
|
737
|
+
this.active = this.againCount > 0 ? this.takeAgain() : this.quic.nextStreamEvent()
|
|
738
|
+
if (this.active < 0) {
|
|
739
|
+
return H3_NEED_MORE
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const event: i32 = this.step(this.active)
|
|
743
|
+
if (event !== H3_NEED_MORE) {
|
|
744
|
+
return event
|
|
745
|
+
}
|
|
746
|
+
this.active = -1
|
|
747
|
+
}
|
|
748
|
+
return H3_ERROR
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** The oldest stream on the `again` ring, taken off it. */
|
|
752
|
+
takeAgain(): i64 {
|
|
753
|
+
const size: i32 = toI32(this.again.length)
|
|
754
|
+
const id: i64 = this.again[this.againHead]
|
|
755
|
+
this.againHead = (this.againHead + 1) % size
|
|
756
|
+
this.againCount = this.againCount - 1
|
|
757
|
+
const k: i32 = this.quic.streams.slotOf(id)
|
|
758
|
+
if (k >= 0 && k < toI32(this.flags.length) && this.slotId[k] === id) {
|
|
759
|
+
this.flags[k] = this.flags[k] & ~H3_FLAG_AGAIN
|
|
760
|
+
}
|
|
761
|
+
return id
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Puts stream `id`, slot `k`, on the `again` ring, unless it is there. */
|
|
765
|
+
stepAgain(k: i32, id: i64): void {
|
|
766
|
+
const size: i32 = toI32(this.again.length)
|
|
767
|
+
if ((this.flags[k] & H3_FLAG_AGAIN) !== 0 || this.againCount >= size) {
|
|
768
|
+
return
|
|
769
|
+
}
|
|
770
|
+
this.again[(this.againHead + this.againCount) % size] = id
|
|
771
|
+
this.againCount = this.againCount + 1
|
|
772
|
+
this.flags[k] = this.flags[k] | H3_FLAG_AGAIN
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** The next event stream `id` has, or H3_NEED_MORE when it has nothing more to say now. */
|
|
776
|
+
step(id: i64): i32 {
|
|
777
|
+
const k: i32 = this.quic.streams.slotOf(id)
|
|
778
|
+
if (k < 0 || k >= toI32(this.slotId.length)) {
|
|
779
|
+
return H3_NEED_MORE
|
|
780
|
+
}
|
|
781
|
+
this.slot = k
|
|
782
|
+
if (this.slotId[k] === id && (this.flags[k] & H3_FLAG_WT) !== 0) {
|
|
783
|
+
return this.wtStep(k, id)
|
|
784
|
+
}
|
|
785
|
+
const kind: i64 = id & 3
|
|
786
|
+
if (kind === 3) {
|
|
787
|
+
return this.ownStream(k)
|
|
788
|
+
}
|
|
789
|
+
if (kind === 1) {
|
|
790
|
+
return H3_NEED_MORE
|
|
791
|
+
}
|
|
792
|
+
if (this.slotId[k] !== id) {
|
|
793
|
+
this.open(k, id)
|
|
794
|
+
}
|
|
795
|
+
const phase: i32 = this.phase[k]
|
|
796
|
+
if (phase >= H3_PHASE_UNI_TYPE) {
|
|
797
|
+
return this.uniStep(k, id)
|
|
798
|
+
}
|
|
799
|
+
return this.requestStep(k, id)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** The state of a stream the client just opened in slot `k`. */
|
|
803
|
+
open(k: i32, id: i64): void {
|
|
804
|
+
this.slotId[k] = id
|
|
805
|
+
this.frameType[k] = H3_SKIP_TYPE
|
|
806
|
+
this.frameLeft[k] = -1
|
|
807
|
+
this.dataTotal[k] = 0
|
|
808
|
+
this.contentLength[k] = -1
|
|
809
|
+
this.fillLeft[k] = 0
|
|
810
|
+
this.dataLeft[k] = 0
|
|
811
|
+
this.flags[k] = 0
|
|
812
|
+
this.headLen[k] = 0
|
|
813
|
+
this.pendAt[k] = 0
|
|
814
|
+
this.pendLen[k] = 0
|
|
815
|
+
if (!http3IsRequest(id)) {
|
|
816
|
+
this.phase[k] = H3_PHASE_UNI_TYPE
|
|
817
|
+
return
|
|
818
|
+
}
|
|
819
|
+
this.phase[k] = H3_PHASE_HEADERS
|
|
820
|
+
if (this.goawayId >= 0 && id >= this.goawayId) {
|
|
821
|
+
if (this.webtransport) {
|
|
822
|
+
// A session's new stream may still come after GOAWAY: its first bytes decide.
|
|
823
|
+
this.flags[k] = H3_FLAG_LATE
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
this.reject(k, id)
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
this.flags[k] = H3_FLAG_COUNTED
|
|
830
|
+
this.live = this.live + 1
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** §5.2: a request past the GOAWAY this side sent is refused, both ways, unprocessed. */
|
|
834
|
+
reject(k: i32, id: i64): void {
|
|
835
|
+
this.quic.streamReset(id, H3_REQUEST_REJECTED)
|
|
836
|
+
this.quic.streamStopSending(id, H3_REQUEST_REJECTED)
|
|
837
|
+
this.flags[k] = H3_FLAG_SEND_DONE
|
|
838
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
839
|
+
this.rejected = h3Count(this.rejected)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** One of this side's own streams has news: the client may not stop it (§6.2.1, RFC 9204 §4.2). */
|
|
843
|
+
ownStream(k: i32): i32 {
|
|
844
|
+
const stream: QuicStream = this.quic.streams.slots[k]
|
|
845
|
+
if (stream.stopCode >= 0) {
|
|
846
|
+
return this.fail(H3_CLOSED_CRITICAL_STREAM)
|
|
847
|
+
}
|
|
848
|
+
return H3_NEED_MORE
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Lets a slot's request count as finished once both of its sides are done. */
|
|
852
|
+
settle(k: i32): void {
|
|
853
|
+
const done: i32 = H3_FLAG_SEND_DONE | H3_FLAG_RECV_DONE | H3_FLAG_COUNTED
|
|
854
|
+
if ((this.flags[k] & done) === done) {
|
|
855
|
+
this.flags[k] = this.flags[k] & ~H3_FLAG_COUNTED
|
|
856
|
+
this.live = this.live - 1
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Marks flags on slot `k` and settles it. */
|
|
861
|
+
mark(k: i32, bits: i32): void {
|
|
862
|
+
this.flags[k] = this.flags[k] | bits
|
|
863
|
+
this.settle(k)
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Reads a frame header of stream `id` into slot `k`'s `head`, a varint at
|
|
868
|
+
* a time — its first byte, which says its size, then the rest — so a
|
|
869
|
+
* header split across STREAM frames costs nothing to keep. Answers 1 with
|
|
870
|
+
* `header` filled, 0 when more is needed, or the `QUIC_STREAM_*` answer
|
|
871
|
+
* that ended the read.
|
|
872
|
+
*/
|
|
873
|
+
readHeader(k: i32, id: i64): i32 {
|
|
874
|
+
const base: i32 = k * H3_HEAD
|
|
875
|
+
while (this.headLen[k] < H3_HEAD) {
|
|
876
|
+
const have: i32 = this.headLen[k]
|
|
877
|
+
// The type's size from its first byte, then the length's from its.
|
|
878
|
+
let need: i32 = 1
|
|
879
|
+
if (have > 0) {
|
|
880
|
+
const typeSize: i32 = h3VarintLength(this.head, base)
|
|
881
|
+
need = have < typeSize ? typeSize - have : 1
|
|
882
|
+
if (have > typeSize) {
|
|
883
|
+
need = typeSize + h3VarintLength(this.head, base + typeSize) - have
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const n: i32 = this.quic.streamRead(id, this.head, base + have, need)
|
|
887
|
+
if (n <= 0) {
|
|
888
|
+
return n
|
|
889
|
+
}
|
|
890
|
+
this.headLen[k] = have + n
|
|
891
|
+
if (h3ReadFrameHeader(this.header, this.head, base, this.headLen[k]) > 0) {
|
|
892
|
+
this.headLen[k] = 0
|
|
893
|
+
return 1
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return H3_ZERO
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ---- Unidirectional streams ------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
/** The next event of the client's unidirectional stream `id` in slot `k`. */
|
|
902
|
+
uniStep(k: i32, id: i64): i32 {
|
|
903
|
+
while (this.state !== H3_STATE_FAILED) {
|
|
904
|
+
const phase: i32 = this.phase[k]
|
|
905
|
+
if (phase === H3_PHASE_UNI_TYPE) {
|
|
906
|
+
const base: i32 = k * H3_HEAD
|
|
907
|
+
const n: i32 = this.quic.streamRead(id, this.head, base + this.headLen[k], H3_ONE)
|
|
908
|
+
if (n !== 1) {
|
|
909
|
+
// §6.2: a stream may end or be reset before its type; nothing is owed.
|
|
910
|
+
if (n < 0) {
|
|
911
|
+
this.phase[k] = H3_PHASE_DONE
|
|
912
|
+
}
|
|
913
|
+
return H3_NEED_MORE
|
|
914
|
+
}
|
|
915
|
+
this.headLen[k] = this.headLen[k] + 1
|
|
916
|
+
const type: i64 = h3ReadVarint(this.head, base, base + this.headLen[k])
|
|
917
|
+
if (type >= 0) {
|
|
918
|
+
this.headLen[k] = 0
|
|
919
|
+
const event: i32 = this.typed(k, id, type)
|
|
920
|
+
if (event !== H3_NEED_MORE) {
|
|
921
|
+
return event
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} else if (phase === H3_PHASE_CONTROL) {
|
|
925
|
+
return this.controlStep(k, id)
|
|
926
|
+
} else if (phase === H3_PHASE_ENCODER || phase === H3_PHASE_DECODER) {
|
|
927
|
+
const n: i32 = this.quic.streamRead(id, this.data, H3_ZERO, toI32(this.data.length))
|
|
928
|
+
if (n === 0) {
|
|
929
|
+
return H3_NEED_MORE
|
|
930
|
+
}
|
|
931
|
+
if (n < 0) {
|
|
932
|
+
return this.fail(H3_CLOSED_CRITICAL_STREAM)
|
|
933
|
+
}
|
|
934
|
+
const result: i64 =
|
|
935
|
+
phase === H3_PHASE_ENCODER
|
|
936
|
+
? this.decoder.receiveEncoderStream(this.data, H3_ZERO, n)
|
|
937
|
+
: this.encoder.receiveDecoderStream(this.data, H3_ZERO, n)
|
|
938
|
+
if (result !== QPACK_OK) {
|
|
939
|
+
return this.fail(result)
|
|
940
|
+
}
|
|
941
|
+
} else if (phase === H3_PHASE_DISCARD) {
|
|
942
|
+
return this.discard(k, id)
|
|
943
|
+
} else {
|
|
944
|
+
return H3_NEED_MORE
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return H3_ERROR
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** The client's unidirectional stream `id` turned out to be of `type` (§6.2). */
|
|
951
|
+
typed(k: i32, id: i64, type: i64): i32 {
|
|
952
|
+
if (type === H3_STREAM_CONTROL) {
|
|
953
|
+
if (this.peerControl >= 0) {
|
|
954
|
+
return this.fail(H3_STREAM_CREATION_ERROR)
|
|
955
|
+
}
|
|
956
|
+
this.peerControl = id
|
|
957
|
+
this.phase[k] = H3_PHASE_CONTROL
|
|
958
|
+
return H3_NEED_MORE
|
|
959
|
+
}
|
|
960
|
+
if (type === H3_STREAM_QPACK_ENCODER) {
|
|
961
|
+
if (this.peerEncoder >= 0) {
|
|
962
|
+
return this.fail(H3_STREAM_CREATION_ERROR)
|
|
963
|
+
}
|
|
964
|
+
this.peerEncoder = id
|
|
965
|
+
this.phase[k] = H3_PHASE_ENCODER
|
|
966
|
+
return H3_NEED_MORE
|
|
967
|
+
}
|
|
968
|
+
if (type === H3_STREAM_QPACK_DECODER) {
|
|
969
|
+
if (this.peerDecoder >= 0) {
|
|
970
|
+
return this.fail(H3_STREAM_CREATION_ERROR)
|
|
971
|
+
}
|
|
972
|
+
this.peerDecoder = id
|
|
973
|
+
this.phase[k] = H3_PHASE_DECODER
|
|
974
|
+
return H3_NEED_MORE
|
|
975
|
+
}
|
|
976
|
+
if (type === H3_STREAM_PUSH) {
|
|
977
|
+
// §6.2.2: only a server pushes.
|
|
978
|
+
return this.fail(H3_STREAM_CREATION_ERROR)
|
|
979
|
+
}
|
|
980
|
+
if (type === H3_STREAM_WEBTRANSPORT && this.webtransport) {
|
|
981
|
+
// draft-02 §4.1: the session ID follows the type. The stream has no side for this one to write.
|
|
982
|
+
this.flags[k] = H3_FLAG_WT | H3_FLAG_SEND_DONE
|
|
983
|
+
this.phase[k] = H3_PHASE_WT_SESSION
|
|
984
|
+
return this.wtStep(k, id)
|
|
985
|
+
}
|
|
986
|
+
// §6.2: a type this side does not know is read and dropped, and its sender asked to stop.
|
|
987
|
+
this.quic.streamStopSending(id, H3_STREAM_CREATION_ERROR)
|
|
988
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
989
|
+
return H3_NEED_MORE
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** Frames on the client's control stream (§6.2.1, §7.2). */
|
|
993
|
+
controlStep(k: i32, id: i64): i32 {
|
|
994
|
+
while (this.state !== H3_STATE_FAILED) {
|
|
995
|
+
if (this.frameLeft[k] < 0) {
|
|
996
|
+
const got: i32 = this.readHeader(k, id)
|
|
997
|
+
if (got === 0) {
|
|
998
|
+
return H3_NEED_MORE
|
|
999
|
+
}
|
|
1000
|
+
if (got < 0) {
|
|
1001
|
+
return this.fail(H3_CLOSED_CRITICAL_STREAM)
|
|
1002
|
+
}
|
|
1003
|
+
const type: i64 = this.header.type
|
|
1004
|
+
const length: i64 = this.header.length
|
|
1005
|
+
if (!this.settingsSeen && type !== H3_FRAME_SETTINGS) {
|
|
1006
|
+
return this.fail(H3_MISSING_SETTINGS)
|
|
1007
|
+
}
|
|
1008
|
+
if (
|
|
1009
|
+
type === H3_FRAME_DATA ||
|
|
1010
|
+
type === H3_FRAME_HEADERS ||
|
|
1011
|
+
type === H3_FRAME_PUSH_PROMISE ||
|
|
1012
|
+
h3ReservedHttp2Frame(type) ||
|
|
1013
|
+
(type === H3_FRAME_SETTINGS && this.settingsSeen)
|
|
1014
|
+
) {
|
|
1015
|
+
return this.fail(H3_FRAME_UNEXPECTED)
|
|
1016
|
+
}
|
|
1017
|
+
const whole: boolean =
|
|
1018
|
+
type === H3_FRAME_SETTINGS ||
|
|
1019
|
+
type === H3_FRAME_GOAWAY ||
|
|
1020
|
+
type === H3_FRAME_MAX_PUSH_ID ||
|
|
1021
|
+
type === H3_FRAME_CANCEL_PUSH
|
|
1022
|
+
if (whole && length > toI64(H3_CONTROL_FRAME_MAX)) {
|
|
1023
|
+
return this.fail(type === H3_FRAME_SETTINGS ? H3_EXCESSIVE_LOAD : H3_FRAME_ERROR)
|
|
1024
|
+
}
|
|
1025
|
+
this.frameType[k] = whole ? type : H3_SKIP_TYPE
|
|
1026
|
+
this.frameLeft[k] = length
|
|
1027
|
+
this.controlFill = 0
|
|
1028
|
+
}
|
|
1029
|
+
if (this.frameLeft[k] > 0) {
|
|
1030
|
+
const into: u8[] = this.frameType[k] === H3_SKIP_TYPE ? this.data : this.control
|
|
1031
|
+
const at: i32 = this.frameType[k] === H3_SKIP_TYPE ? H3_ZERO : this.controlFill
|
|
1032
|
+
const want: i32 = toI32(http3Min(this.frameLeft[k], toI64(toI32(into.length) - at)))
|
|
1033
|
+
const n: i32 = this.quic.streamRead(id, into, at, want)
|
|
1034
|
+
if (n === 0) {
|
|
1035
|
+
return H3_NEED_MORE
|
|
1036
|
+
}
|
|
1037
|
+
if (n < 0) {
|
|
1038
|
+
return this.fail(H3_CLOSED_CRITICAL_STREAM)
|
|
1039
|
+
}
|
|
1040
|
+
this.frameLeft[k] = this.frameLeft[k] - toI64(n)
|
|
1041
|
+
if (this.frameType[k] !== H3_SKIP_TYPE) {
|
|
1042
|
+
this.controlFill = this.controlFill + n
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (this.frameLeft[k] === 0) {
|
|
1046
|
+
this.frameLeft[k] = -1
|
|
1047
|
+
const event: i32 = this.controlFrame(this.frameType[k])
|
|
1048
|
+
if (event !== H3_NEED_MORE) {
|
|
1049
|
+
return event
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return H3_ERROR
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Acts on a whole control frame of `type` in `control[0 .. controlFill)`. */
|
|
1057
|
+
controlFrame(type: i64): i32 {
|
|
1058
|
+
if (type === H3_FRAME_SETTINGS) {
|
|
1059
|
+
const result: i64 = h3ReadSettings(this.peer, this.control, H3_ZERO, this.controlFill)
|
|
1060
|
+
if (result !== H3_SETTINGS_OK) {
|
|
1061
|
+
return this.fail(result)
|
|
1062
|
+
}
|
|
1063
|
+
// RFC 9220 §5 and RFC 9297 §2.1.1: each is 0 or 1.
|
|
1064
|
+
if (this.peer.enableConnectProtocol > 1 || this.peer.h3Datagram > 1) {
|
|
1065
|
+
return this.fail(H3_SETTINGS_ERROR)
|
|
1066
|
+
}
|
|
1067
|
+
this.settingsSeen = true
|
|
1068
|
+
// The requests that waited for these SETTINGS are stepped again.
|
|
1069
|
+
for (let j: i32 = 0; j < this.waitCount && j < toI32(this.waiting.length); j++) {
|
|
1070
|
+
const id: i64 = this.waiting[j]
|
|
1071
|
+
const w: i32 = this.quic.streams.slotOf(id)
|
|
1072
|
+
if (w >= 0 && w < toI32(this.flags.length) && this.slotId[w] === id) {
|
|
1073
|
+
this.flags[w] = this.flags[w] & ~H3_FLAG_WAITING
|
|
1074
|
+
this.stepAgain(w, id)
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
this.waitCount = 0
|
|
1078
|
+
return H3_NEED_MORE
|
|
1079
|
+
}
|
|
1080
|
+
if (type === H3_SKIP_TYPE) {
|
|
1081
|
+
return H3_NEED_MORE
|
|
1082
|
+
}
|
|
1083
|
+
const value: i64 = h3ReadIdPayload(this.control, H3_ZERO, this.controlFill)
|
|
1084
|
+
if (value < 0) {
|
|
1085
|
+
return this.fail(H3_FRAME_ERROR)
|
|
1086
|
+
}
|
|
1087
|
+
if (type === H3_FRAME_GOAWAY) {
|
|
1088
|
+
// §5.2: a client's GOAWAY names a push ID, which may only fall.
|
|
1089
|
+
if (this.peerGoaway >= 0 && value > this.peerGoaway) {
|
|
1090
|
+
return this.fail(H3_ID_ERROR)
|
|
1091
|
+
}
|
|
1092
|
+
this.peerGoaway = value
|
|
1093
|
+
this.lastStreamId = value
|
|
1094
|
+
return H3_GOAWAY
|
|
1095
|
+
}
|
|
1096
|
+
if (type === H3_FRAME_MAX_PUSH_ID) {
|
|
1097
|
+
// §7.2.7: it may not fall. This side never pushes, so that is all it means.
|
|
1098
|
+
if (value < this.maxPushId) {
|
|
1099
|
+
return this.fail(H3_ID_ERROR)
|
|
1100
|
+
}
|
|
1101
|
+
this.maxPushId = value
|
|
1102
|
+
return H3_NEED_MORE
|
|
1103
|
+
}
|
|
1104
|
+
// CANCEL_PUSH (§7.2.3): no push was ever promised, so only one past the
|
|
1105
|
+
// push IDs allowed is an error; any other is a no-op.
|
|
1106
|
+
return value > this.maxPushId ? this.fail(H3_ID_ERROR) : H3_NEED_MORE
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** Reads and drops what is left of stream `id`, slot `k`, to its end or reset. */
|
|
1110
|
+
discard(k: i32, id: i64): i32 {
|
|
1111
|
+
const n: i32 = this.drop(id)
|
|
1112
|
+
if (n < 0) {
|
|
1113
|
+
this.phase[k] = H3_PHASE_DONE
|
|
1114
|
+
this.mark(k, H3_FLAG_RECV_DONE)
|
|
1115
|
+
}
|
|
1116
|
+
return this.writable(k, id)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Reads stream `id` into `data` until nothing more is there; answers 0, or the `QUIC_STREAM_*` that ended it. */
|
|
1120
|
+
drop(id: i64): i32 {
|
|
1121
|
+
let n: i32 = this.quic.streamRead(id, this.data, H3_ZERO, toI32(this.data.length))
|
|
1122
|
+
while (n > 0) {
|
|
1123
|
+
n = this.quic.streamRead(id, this.data, H3_ZERO, toI32(this.data.length))
|
|
1124
|
+
}
|
|
1125
|
+
return n
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ---- WebTransport streams ----------------------------------------------------------
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Stream `id`, slot `k`, named session `session` in its first bytes: it is
|
|
1132
|
+
* held, unread, and the program told with H3_STREAM. A session ID that is
|
|
1133
|
+
* not a client-initiated bidirectional stream is H3_ID_ERROR (draft-02 §4).
|
|
1134
|
+
*/
|
|
1135
|
+
held(k: i32, id: i64, session: i64): i32 {
|
|
1136
|
+
if (!http3IsRequest(session)) {
|
|
1137
|
+
return this.fail(H3_ID_ERROR)
|
|
1138
|
+
}
|
|
1139
|
+
if (this.goawayId >= 0 && session >= this.goawayId) {
|
|
1140
|
+
// A session at or past this side's GOAWAY can never come (its CONNECT is refused), so its stream is gone.
|
|
1141
|
+
this.abandon(k, id, H3_WT_SESSION_GONE)
|
|
1142
|
+
this.stopReading(k, id, H3_WT_SESSION_GONE)
|
|
1143
|
+
this.rejected = h3Count(this.rejected)
|
|
1144
|
+
return H3_NEED_MORE
|
|
1145
|
+
}
|
|
1146
|
+
this.phase[k] = H3_PHASE_WT_HELD
|
|
1147
|
+
this.frameLeft[k] = -1
|
|
1148
|
+
this.stream = id
|
|
1149
|
+
this.session = session
|
|
1150
|
+
return H3_STREAM
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* The next event of WebTransport stream `id` in slot `k`: its session ID
|
|
1155
|
+
* (a unidirectional stream's), then its bytes as H3_DATA, H3_END at its
|
|
1156
|
+
* FIN and H3_RESET at the client's RESET_STREAM; H3_STOPPED once for the
|
|
1157
|
+
* client's STOP_SENDING; and H3_WRITABLE.
|
|
1158
|
+
*/
|
|
1159
|
+
wtStep(k: i32, id: i64): i32 {
|
|
1160
|
+
const stream: QuicStream = this.quic.streams.slots[k]
|
|
1161
|
+
if (stream.stopCode >= 0 && (this.flags[k] & H3_FLAG_SEND_DONE) === 0) {
|
|
1162
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
1163
|
+
this.stream = id
|
|
1164
|
+
this.errorCode = stream.stopCode
|
|
1165
|
+
return H3_STOPPED
|
|
1166
|
+
}
|
|
1167
|
+
const phase: i32 = this.phase[k]
|
|
1168
|
+
if (phase === H3_PHASE_WT_SESSION) {
|
|
1169
|
+
const base: i32 = k * H3_HEAD
|
|
1170
|
+
while (this.headLen[k] < 8) {
|
|
1171
|
+
const n: i32 = this.quic.streamRead(id, this.head, base + this.headLen[k], H3_ONE)
|
|
1172
|
+
if (n !== 1) {
|
|
1173
|
+
// Ended or reset before its session ID: nothing is owed (§6.2).
|
|
1174
|
+
if (n < 0) {
|
|
1175
|
+
this.phase[k] = H3_PHASE_DONE
|
|
1176
|
+
this.mark(k, H3_FLAG_RECV_DONE)
|
|
1177
|
+
}
|
|
1178
|
+
return H3_NEED_MORE
|
|
1179
|
+
}
|
|
1180
|
+
this.headLen[k] = this.headLen[k] + 1
|
|
1181
|
+
const session: i64 = h3ReadVarint(this.head, base, base + this.headLen[k])
|
|
1182
|
+
if (session >= 0) {
|
|
1183
|
+
this.headLen[k] = 0
|
|
1184
|
+
return this.held(k, id, session)
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return H3_NEED_MORE
|
|
1188
|
+
}
|
|
1189
|
+
if (phase === H3_PHASE_WT) {
|
|
1190
|
+
if ((this.flags[k] & H3_FLAG_RECV_DONE) === 0) {
|
|
1191
|
+
const resetCode: i64 = stream.resetCode
|
|
1192
|
+
const n: i32 = this.quic.streamRead(id, this.data, H3_ZERO, toI32(this.data.length))
|
|
1193
|
+
if (n > 0) {
|
|
1194
|
+
this.stream = id
|
|
1195
|
+
this.dataStart = 0
|
|
1196
|
+
this.dataLength = n
|
|
1197
|
+
return H3_DATA
|
|
1198
|
+
}
|
|
1199
|
+
if (n < 0) {
|
|
1200
|
+
this.phase[k] = H3_PHASE_DONE
|
|
1201
|
+
this.mark(k, H3_FLAG_RECV_DONE)
|
|
1202
|
+
if (n === QUIC_STREAM_END) {
|
|
1203
|
+
this.stream = id
|
|
1204
|
+
return H3_END
|
|
1205
|
+
}
|
|
1206
|
+
if (n === QUIC_STREAM_ERR_RESET) {
|
|
1207
|
+
return this.resetEvent(id, resetCode, true)
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return this.writable(k, id)
|
|
1212
|
+
}
|
|
1213
|
+
if (phase === H3_PHASE_DISCARD) {
|
|
1214
|
+
return this.discard(k, id)
|
|
1215
|
+
}
|
|
1216
|
+
return phase === H3_PHASE_DONE ? this.writable(k, id) : H3_NEED_MORE
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** The slot of WebTransport stream `id` when it is known here, else -1. */
|
|
1220
|
+
wtSlot(id: i64): i32 {
|
|
1221
|
+
if (this.state === H3_STATE_FAILED) {
|
|
1222
|
+
return -1
|
|
1223
|
+
}
|
|
1224
|
+
const k: i32 = this.quic.streams.slotOf(id)
|
|
1225
|
+
if (
|
|
1226
|
+
k < 0 ||
|
|
1227
|
+
k >= toI32(this.slotId.length) ||
|
|
1228
|
+
this.slotId[k] !== id ||
|
|
1229
|
+
(this.flags[k] & H3_FLAG_WT) === 0
|
|
1230
|
+
) {
|
|
1231
|
+
return -1
|
|
1232
|
+
}
|
|
1233
|
+
return k
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Lets a stream H3_STREAM held go: its bytes come as H3_DATA from the next
|
|
1238
|
+
* `next()`, and the program may write it. Answers 0, or H3_CLOSED for a
|
|
1239
|
+
* stream that is not one held.
|
|
1240
|
+
*/
|
|
1241
|
+
acceptStream(id: i64): i32 {
|
|
1242
|
+
const k: i32 = this.wtSlot(id)
|
|
1243
|
+
if (k < 0 || this.phase[k] !== H3_PHASE_WT_HELD) {
|
|
1244
|
+
return H3_CLOSED
|
|
1245
|
+
}
|
|
1246
|
+
this.phase[k] = H3_PHASE_WT
|
|
1247
|
+
this.stepAgain(k, id)
|
|
1248
|
+
return 0
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Refuses WebTransport stream `id` with `code`: its side here is reset,
|
|
1253
|
+
* the client's asked to stop, and what it sent read and dropped. Answers 0,
|
|
1254
|
+
* or H3_CLOSED for a stream that is not a WebTransport one.
|
|
1255
|
+
*/
|
|
1256
|
+
refuseStream(id: i64, code: i64): i32 {
|
|
1257
|
+
const k: i32 = this.wtSlot(id)
|
|
1258
|
+
if (k < 0) {
|
|
1259
|
+
return H3_CLOSED
|
|
1260
|
+
}
|
|
1261
|
+
this.abandon(k, id, code)
|
|
1262
|
+
this.stopReading(k, id, code)
|
|
1263
|
+
return 0
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/** Asks the client to stop stream `id`, slot `k`, with `code`, and drops what it still sends, unless its side is over or being dropped. */
|
|
1267
|
+
stopReading(k: i32, id: i64, code: i64): void {
|
|
1268
|
+
if (this.phase[k] !== H3_PHASE_DONE && this.phase[k] !== H3_PHASE_DISCARD) {
|
|
1269
|
+
this.quic.streamStopSending(id, code)
|
|
1270
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
1271
|
+
this.stepAgain(k, id)
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Opens a WebTransport stream of this side's for `session` (draft-02
|
|
1277
|
+
* §4.1, §4.2): unidirectional with the type 0x54, or bidirectional with
|
|
1278
|
+
* the signal 0x41, and the session ID, which go before anything written.
|
|
1279
|
+
* Answers its ID, H3_AGAIN while the client's MAX_STREAMS allows no more,
|
|
1280
|
+
* or H3_CLOSED with WebTransport off or the connection failed.
|
|
1281
|
+
*/
|
|
1282
|
+
openStream(session: i64, bidirectional: boolean): i64 {
|
|
1283
|
+
if (!this.webtransport || this.state === H3_STATE_FAILED || !http3IsRequest(session) || session < 0) {
|
|
1284
|
+
return toI64(H3_CLOSED)
|
|
1285
|
+
}
|
|
1286
|
+
const id: i64 = this.quic.openStream(bidirectional)
|
|
1287
|
+
const k: i32 = id >= 0 ? this.quic.streams.slotOf(id) : H3_NONE32
|
|
1288
|
+
if (k < 0 || k >= toI32(this.slotId.length)) {
|
|
1289
|
+
return toI64(H3_AGAIN)
|
|
1290
|
+
}
|
|
1291
|
+
this.open(k, id)
|
|
1292
|
+
// A unidirectional stream of this side's has no side for the client to write.
|
|
1293
|
+
this.flags[k] = bidirectional ? H3_FLAG_WT : H3_FLAG_WT | H3_FLAG_RECV_DONE
|
|
1294
|
+
this.phase[k] = bidirectional ? H3_PHASE_WT : H3_PHASE_DONE
|
|
1295
|
+
const end: i32 = toI32(this.scratch.length)
|
|
1296
|
+
const p: i32 = h3PutVarint(
|
|
1297
|
+
this.scratch,
|
|
1298
|
+
H3_ZERO,
|
|
1299
|
+
end,
|
|
1300
|
+
bidirectional ? H3_FRAME_WEBTRANSPORT_STREAM : H3_STREAM_WEBTRANSPORT
|
|
1301
|
+
)
|
|
1302
|
+
this.owe(k, this.scratch, h3PutVarint(this.scratch, p, end, session))
|
|
1303
|
+
if (!this.drain(k, id)) {
|
|
1304
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1305
|
+
}
|
|
1306
|
+
return id
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Writes up to `len` bytes of `buf` from `off` to WebTransport stream `id`,
|
|
1311
|
+
* as they are, and its FIN when `fin` and every byte was taken. Answers how
|
|
1312
|
+
* many it took — fewer only when the send buffer filled, and H3_WRITABLE
|
|
1313
|
+
* then names the stream — or H3_AGAIN when it took none, or H3_CLOSED for
|
|
1314
|
+
* a stream with no side here to write, finished, reset, held or stopped.
|
|
1315
|
+
*/
|
|
1316
|
+
writeStream(id: i64, buf: u8[], off: i32, len: i32, fin: boolean): i32 {
|
|
1317
|
+
h3CheckWindow("Http3Connection.writeStream", buf, off, len)
|
|
1318
|
+
const k: i32 = this.wtSlot(id)
|
|
1319
|
+
if (
|
|
1320
|
+
k < 0 ||
|
|
1321
|
+
(this.flags[k] & H3_FLAG_SEND_DONE) !== 0 ||
|
|
1322
|
+
this.phase[k] === H3_PHASE_WT_HELD ||
|
|
1323
|
+
this.quic.streams.slots[k].stopCode >= 0
|
|
1324
|
+
) {
|
|
1325
|
+
return H3_CLOSED
|
|
1326
|
+
}
|
|
1327
|
+
if (!this.drain(k, id)) {
|
|
1328
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1329
|
+
return H3_AGAIN
|
|
1330
|
+
}
|
|
1331
|
+
const n: i32 = this.quic.streamWrite(id, buf, off, len, fin)
|
|
1332
|
+
if (n < 0) {
|
|
1333
|
+
return H3_CLOSED
|
|
1334
|
+
}
|
|
1335
|
+
if (n < len) {
|
|
1336
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1337
|
+
} else if (fin) {
|
|
1338
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
1339
|
+
}
|
|
1340
|
+
return n > 0 || (fin && len === 0) ? n : H3_AGAIN
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Resets this side of WebTransport stream `id` with `code`; the client's side goes on. Answers 0 or H3_CLOSED. */
|
|
1344
|
+
resetStream(id: i64, code: i64): i32 {
|
|
1345
|
+
const k: i32 = this.wtSlot(id)
|
|
1346
|
+
if (k < 0) {
|
|
1347
|
+
return H3_CLOSED
|
|
1348
|
+
}
|
|
1349
|
+
this.abandon(k, id, code)
|
|
1350
|
+
return 0
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Asks the client to stop its side of WebTransport stream `id` with
|
|
1355
|
+
* `code`, and drops what it still sends; this side's goes on. Answers 0 or
|
|
1356
|
+
* H3_CLOSED.
|
|
1357
|
+
*/
|
|
1358
|
+
stopStream(id: i64, code: i64): i32 {
|
|
1359
|
+
const k: i32 = this.wtSlot(id)
|
|
1360
|
+
if (k < 0) {
|
|
1361
|
+
return H3_CLOSED
|
|
1362
|
+
}
|
|
1363
|
+
this.stopReading(k, id, code)
|
|
1364
|
+
return 0
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// ---- Request streams ------------------------------------------------------------
|
|
1368
|
+
|
|
1369
|
+
/** The next event of request stream `id` in slot `k`. */
|
|
1370
|
+
requestStep(k: i32, id: i64): i32 {
|
|
1371
|
+
const stream: QuicStream = this.quic.streams.slots[k]
|
|
1372
|
+
while (this.state !== H3_STATE_FAILED) {
|
|
1373
|
+
const phase: i32 = this.phase[k]
|
|
1374
|
+
if (stream.stopCode >= 0 && (this.flags[k] & H3_FLAG_SEND_DONE) === 0) {
|
|
1375
|
+
// The client stopped the response (QUIC answered with RESET_STREAM, §3.5 of RFC 9000).
|
|
1376
|
+
const told: boolean = (this.flags[k] & H3_FLAG_DELIVERED) !== 0
|
|
1377
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
1378
|
+
if (told) {
|
|
1379
|
+
if (phase !== H3_PHASE_DONE) {
|
|
1380
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
1381
|
+
}
|
|
1382
|
+
return this.resetEvent(id, stream.stopCode, true)
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (phase === H3_PHASE_DONE) {
|
|
1386
|
+
return this.writable(k, id)
|
|
1387
|
+
}
|
|
1388
|
+
if (phase === H3_PHASE_DISCARD) {
|
|
1389
|
+
return this.discard(k, id)
|
|
1390
|
+
}
|
|
1391
|
+
if (this.webtransport && !this.settingsSeen && phase === H3_PHASE_HEADERS && this.headLen[k] === 0) {
|
|
1392
|
+
// draft-02 §3.1: with WebTransport on, a request waits for the client's SETTINGS, which say whether it may be a session.
|
|
1393
|
+
if ((this.flags[k] & H3_FLAG_WAITING) === 0 && this.waitCount < toI32(this.waiting.length)) {
|
|
1394
|
+
this.waiting[this.waitCount] = id
|
|
1395
|
+
this.waitCount = this.waitCount + 1
|
|
1396
|
+
this.flags[k] = this.flags[k] | H3_FLAG_WAITING
|
|
1397
|
+
}
|
|
1398
|
+
return H3_NEED_MORE
|
|
1399
|
+
}
|
|
1400
|
+
if (this.frameLeft[k] < 0) {
|
|
1401
|
+
const event: i32 = this.requestFrame(k, id)
|
|
1402
|
+
if (event === H3_CONTINUE) {
|
|
1403
|
+
continue
|
|
1404
|
+
}
|
|
1405
|
+
if (event !== H3_NEED_MORE) {
|
|
1406
|
+
return event
|
|
1407
|
+
}
|
|
1408
|
+
// The header is not all in yet, unless the stream moved on to another phase.
|
|
1409
|
+
if (this.phase[k] === phase) {
|
|
1410
|
+
return this.writable(k, id)
|
|
1411
|
+
}
|
|
1412
|
+
continue
|
|
1413
|
+
}
|
|
1414
|
+
const type: i64 = this.frameType[k]
|
|
1415
|
+
if (type === H3_FRAME_HEADERS) {
|
|
1416
|
+
// A field section is decoded whole: wait until all of it is in.
|
|
1417
|
+
if (stream.resetCode < 0 && stream.readable() < this.frameLeft[k]) {
|
|
1418
|
+
if (stream.recvFinal >= 0 && stream.recvRead + this.frameLeft[k] > stream.recvFinal) {
|
|
1419
|
+
return this.fail(H3_FRAME_ERROR)
|
|
1420
|
+
}
|
|
1421
|
+
return this.writable(k, id)
|
|
1422
|
+
}
|
|
1423
|
+
const length: i32 = toI32(this.frameLeft[k])
|
|
1424
|
+
const n: i32 = this.quic.streamRead(id, this.section, H3_ZERO, length)
|
|
1425
|
+
if (n < 0) {
|
|
1426
|
+
return this.ended(k, id, n, stream.resetCode)
|
|
1427
|
+
}
|
|
1428
|
+
if (n !== length) {
|
|
1429
|
+
return this.writable(k, id)
|
|
1430
|
+
}
|
|
1431
|
+
this.frameLeft[k] = -1
|
|
1432
|
+
const event: i32 = this.fieldSection(k, id, length)
|
|
1433
|
+
if (event !== H3_NEED_MORE) {
|
|
1434
|
+
return event
|
|
1435
|
+
}
|
|
1436
|
+
continue
|
|
1437
|
+
}
|
|
1438
|
+
// DATA, a payload being skipped, or an unknown frame's.
|
|
1439
|
+
const want: i32 = toI32(http3Min(this.frameLeft[k], toI64(toI32(this.data.length))))
|
|
1440
|
+
const resetCode: i64 = stream.resetCode
|
|
1441
|
+
const n: i32 = this.quic.streamRead(id, this.data, H3_ZERO, want)
|
|
1442
|
+
if (n === 0) {
|
|
1443
|
+
return this.writable(k, id)
|
|
1444
|
+
}
|
|
1445
|
+
if (n < 0) {
|
|
1446
|
+
return n === QUIC_STREAM_END ? this.fail(H3_FRAME_ERROR) : this.ended(k, id, n, resetCode)
|
|
1447
|
+
}
|
|
1448
|
+
this.frameLeft[k] = this.frameLeft[k] - toI64(n)
|
|
1449
|
+
if (type === H3_FRAME_DATA) {
|
|
1450
|
+
if (this.frameLeft[k] === 0) {
|
|
1451
|
+
this.frameLeft[k] = -1
|
|
1452
|
+
}
|
|
1453
|
+
this.dataTotal[k] = this.dataTotal[k] + toI64(n)
|
|
1454
|
+
if (this.contentLength[k] >= 0 && this.dataTotal[k] > this.contentLength[k]) {
|
|
1455
|
+
return this.streamError(k, id, H3_MESSAGE_ERROR)
|
|
1456
|
+
}
|
|
1457
|
+
this.stream = id
|
|
1458
|
+
this.dataStart = 0
|
|
1459
|
+
this.dataLength = n
|
|
1460
|
+
return H3_DATA
|
|
1461
|
+
}
|
|
1462
|
+
if (this.frameLeft[k] === 0) {
|
|
1463
|
+
this.frameLeft[k] = -1
|
|
1464
|
+
if (this.phase[k] === H3_PHASE_SKIP) {
|
|
1465
|
+
return this.answerTooLarge(k, id)
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return H3_ERROR
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Reads the next frame header of request stream `id` and decides what its
|
|
1474
|
+
* payload is (§4.1, §7.2): DATA after HEADERS, HEADERS first or as
|
|
1475
|
+
* trailers, and anything else unknown and skipped, or refused where
|
|
1476
|
+
* RFC 9114 says so. A stream that ends here ends the request.
|
|
1477
|
+
*/
|
|
1478
|
+
requestFrame(k: i32, id: i64): i32 {
|
|
1479
|
+
const stream: QuicStream = this.quic.streams.slots[k]
|
|
1480
|
+
const resetCode: i64 = stream.resetCode
|
|
1481
|
+
const got: i32 = this.readHeader(k, id)
|
|
1482
|
+
if (got === 0) {
|
|
1483
|
+
return H3_NEED_MORE
|
|
1484
|
+
}
|
|
1485
|
+
if (got < 0) {
|
|
1486
|
+
if (got === QUIC_STREAM_END && this.headLen[k] > 0) {
|
|
1487
|
+
return this.fail(H3_FRAME_ERROR)
|
|
1488
|
+
}
|
|
1489
|
+
if ((this.flags[k] & H3_FLAG_LATE) !== 0) {
|
|
1490
|
+
this.reject(k, id)
|
|
1491
|
+
return this.dropped(id)
|
|
1492
|
+
}
|
|
1493
|
+
return this.ended(k, id, got, resetCode)
|
|
1494
|
+
}
|
|
1495
|
+
const type: i64 = this.header.type
|
|
1496
|
+
const length: i64 = this.header.length
|
|
1497
|
+
const phase: i32 = this.phase[k]
|
|
1498
|
+
if (type === H3_FRAME_WEBTRANSPORT_STREAM && this.webtransport) {
|
|
1499
|
+
if (phase !== H3_PHASE_HEADERS) {
|
|
1500
|
+
// draft-02 §4.2: the signal opens a stream, and is nothing anywhere else.
|
|
1501
|
+
return this.fail(H3_FRAME_ERROR)
|
|
1502
|
+
}
|
|
1503
|
+
// Its "length" is the session ID. A stream past this side's GOAWAY counts once it is held, for a session below it.
|
|
1504
|
+
this.flags[k] = this.flags[k] | H3_FLAG_WT
|
|
1505
|
+
const event: i32 = this.held(k, id, length)
|
|
1506
|
+
if (event === H3_STREAM && (this.flags[k] & H3_FLAG_LATE) !== 0) {
|
|
1507
|
+
this.flags[k] = (this.flags[k] & ~H3_FLAG_LATE) | H3_FLAG_COUNTED
|
|
1508
|
+
this.live = this.live + 1
|
|
1509
|
+
}
|
|
1510
|
+
return event
|
|
1511
|
+
}
|
|
1512
|
+
if ((this.flags[k] & H3_FLAG_LATE) !== 0) {
|
|
1513
|
+
this.reject(k, id)
|
|
1514
|
+
return this.dropped(id)
|
|
1515
|
+
}
|
|
1516
|
+
if (type === H3_FRAME_DATA) {
|
|
1517
|
+
if (phase !== H3_PHASE_BODY) {
|
|
1518
|
+
return this.fail(H3_FRAME_UNEXPECTED)
|
|
1519
|
+
}
|
|
1520
|
+
this.frameType[k] = type
|
|
1521
|
+
this.frameLeft[k] = length > 0 ? length : -1
|
|
1522
|
+
return H3_CONTINUE
|
|
1523
|
+
}
|
|
1524
|
+
if (type === H3_FRAME_HEADERS) {
|
|
1525
|
+
if (phase === H3_PHASE_TRAILED) {
|
|
1526
|
+
return this.fail(H3_FRAME_UNEXPECTED)
|
|
1527
|
+
}
|
|
1528
|
+
if (length > toI64(this.config.maxFieldSectionSize)) {
|
|
1529
|
+
if (phase === H3_PHASE_HEADERS) {
|
|
1530
|
+
// §4.2.2: the section is skipped and answered with a 431.
|
|
1531
|
+
this.phase[k] = H3_PHASE_SKIP
|
|
1532
|
+
this.frameType[k] = H3_SKIP_TYPE
|
|
1533
|
+
this.frameLeft[k] = length
|
|
1534
|
+
return H3_CONTINUE
|
|
1535
|
+
}
|
|
1536
|
+
return this.streamError(k, id, H3_EXCESSIVE_LOAD)
|
|
1537
|
+
}
|
|
1538
|
+
this.frameType[k] = type
|
|
1539
|
+
this.frameLeft[k] = length
|
|
1540
|
+
if (length === 0) {
|
|
1541
|
+
// An empty section has no prefix, which QPACK refuses.
|
|
1542
|
+
this.frameLeft[k] = -1
|
|
1543
|
+
return this.fieldSection(k, id, H3_ZERO)
|
|
1544
|
+
}
|
|
1545
|
+
return H3_CONTINUE
|
|
1546
|
+
}
|
|
1547
|
+
if (
|
|
1548
|
+
type === H3_FRAME_SETTINGS ||
|
|
1549
|
+
type === H3_FRAME_GOAWAY ||
|
|
1550
|
+
type === H3_FRAME_MAX_PUSH_ID ||
|
|
1551
|
+
type === H3_FRAME_CANCEL_PUSH ||
|
|
1552
|
+
type === H3_FRAME_PUSH_PROMISE ||
|
|
1553
|
+
h3ReservedHttp2Frame(type)
|
|
1554
|
+
) {
|
|
1555
|
+
return this.fail(H3_FRAME_UNEXPECTED)
|
|
1556
|
+
}
|
|
1557
|
+
// §9: an unknown frame type is skipped, wherever it comes.
|
|
1558
|
+
this.frameType[k] = H3_SKIP_TYPE
|
|
1559
|
+
this.frameLeft[k] = length > 0 ? length : -1
|
|
1560
|
+
return H3_CONTINUE
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* A whole HEADERS payload of `length` bytes is in `section`: the request's
|
|
1565
|
+
* field section, or its trailers. Decodes it, copies it into `fields` and
|
|
1566
|
+
* checks it there.
|
|
1567
|
+
*/
|
|
1568
|
+
fieldSection(k: i32, id: i64, length: i32): i32 {
|
|
1569
|
+
const result: i64 = this.decoder.decode(this.section, H3_ZERO, length)
|
|
1570
|
+
const first: boolean = this.phase[k] === H3_PHASE_HEADERS
|
|
1571
|
+
if (result === QPACK_SECTION_TOO_LARGE) {
|
|
1572
|
+
return first ? this.answerTooLarge(k, id) : this.streamError(k, id, H3_EXCESSIVE_LOAD)
|
|
1573
|
+
}
|
|
1574
|
+
if (result !== QPACK_OK) {
|
|
1575
|
+
return this.fail(result)
|
|
1576
|
+
}
|
|
1577
|
+
this.copyFields()
|
|
1578
|
+
if (!first) {
|
|
1579
|
+
if (this.fields.readTrailers(this.nameList, this.valueList) !== HTTP_FIELDS_OK) {
|
|
1580
|
+
return this.streamError(k, id, H3_MESSAGE_ERROR)
|
|
1581
|
+
}
|
|
1582
|
+
this.phase[k] = H3_PHASE_TRAILED
|
|
1583
|
+
this.stream = id
|
|
1584
|
+
return H3_TRAILERS
|
|
1585
|
+
}
|
|
1586
|
+
if (this.fields.readRequest(this.nameList, this.valueList, this.connect) !== HTTP_FIELDS_OK) {
|
|
1587
|
+
return this.streamError(k, id, H3_MESSAGE_ERROR)
|
|
1588
|
+
}
|
|
1589
|
+
// An extended CONNECT reaches the program like any request, `fields.protocol` set:
|
|
1590
|
+
// `nish/net/webtransport` makes a session of one whose protocol is `webtransport`.
|
|
1591
|
+
this.contentLength[k] = this.fields.contentLength
|
|
1592
|
+
this.phase[k] = H3_PHASE_BODY
|
|
1593
|
+
this.flags[k] = this.flags[k] | H3_FLAG_DELIVERED
|
|
1594
|
+
this.stream = id
|
|
1595
|
+
return H3_REQUEST
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Copies the decoder's fields into the connection's own arrays and the lists `fields` reads. */
|
|
1599
|
+
copyFields(): void {
|
|
1600
|
+
const d: QpackDecoder = this.decoder
|
|
1601
|
+
http3EmptyList(this.nameList)
|
|
1602
|
+
http3EmptyList(this.valueList)
|
|
1603
|
+
this.poolUsed.fill(H3_ZERO)
|
|
1604
|
+
for (let i: i32 = 0; i < d.count; i++) {
|
|
1605
|
+
this.nameList.push(this.copyOf(d, d.nameStart[i], d.nameLength[i]))
|
|
1606
|
+
this.valueList.push(this.copyOf(d, d.valueStart[i], d.valueLength[i]))
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* A copy of the decoder's bytes `[start, start + length)`, in an array of
|
|
1612
|
+
* the size class `length` falls in: class `j` holds arrays made with room
|
|
1613
|
+
* for 2^j bytes, for strings shorter than that and at least half as long.
|
|
1614
|
+
* A section takes the arrays of each class in order and the next takes the
|
|
1615
|
+
* same ones again, so a class grows only when one section needs more of its
|
|
1616
|
+
* size than any before it, and the total stays bounded by the field-section
|
|
1617
|
+
* cap (`docs/security/http3.md`, H3-2).
|
|
1618
|
+
*/
|
|
1619
|
+
copyOf(d: QpackDecoder, start: i32, length: i32): u8[] {
|
|
1620
|
+
let j: i32 = 0
|
|
1621
|
+
while (j < H3_FIELD_CLASSES - 1 && 1 << j <= length) {
|
|
1622
|
+
j++
|
|
1623
|
+
}
|
|
1624
|
+
const pool: u8[][] = this.pools[j]
|
|
1625
|
+
const used: i32 = this.poolUsed[j]
|
|
1626
|
+
if (used >= toI32(pool.length)) {
|
|
1627
|
+
// Made with room for the class, then emptied below like any reused one.
|
|
1628
|
+
pool.push(new Array<u8>(1 << j))
|
|
1629
|
+
}
|
|
1630
|
+
this.poolUsed[j] = used + 1
|
|
1631
|
+
const out: u8[] = pool[used]
|
|
1632
|
+
http3Empty(out)
|
|
1633
|
+
const from: i32 = start > 0 ? start : H3_ZERO
|
|
1634
|
+
const end: i32 = start + length < toI32(d.bytes.length) ? start + length : toI32(d.bytes.length)
|
|
1635
|
+
for (let k: i32 = from; k < end; k++) {
|
|
1636
|
+
out.push(d.bytes[k])
|
|
1637
|
+
}
|
|
1638
|
+
return out
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* The request stream ended, cleanly (`n` is `QUIC_STREAM_END`) or by the
|
|
1643
|
+
* client's RESET_STREAM with `resetCode`, at a frame boundary.
|
|
1644
|
+
*/
|
|
1645
|
+
ended(k: i32, id: i64, n: i32, resetCode: i64): i32 {
|
|
1646
|
+
const phase: i32 = this.phase[k]
|
|
1647
|
+
this.phase[k] = H3_PHASE_DONE
|
|
1648
|
+
this.mark(k, H3_FLAG_RECV_DONE)
|
|
1649
|
+
const delivered: boolean = (this.flags[k] & H3_FLAG_DELIVERED) !== 0
|
|
1650
|
+
if (n === QUIC_STREAM_ERR_RESET) {
|
|
1651
|
+
// §4.1.1: a response to a request that will not finish is abandoned too.
|
|
1652
|
+
this.abandon(k, id, resetCode === H3_REQUEST_CANCELLED ? H3_REQUEST_CANCELLED : H3_REQUEST_INCOMPLETE)
|
|
1653
|
+
return delivered ? this.resetEvent(id, resetCode, true) : this.dropped(id)
|
|
1654
|
+
}
|
|
1655
|
+
if (n !== QUIC_STREAM_END) {
|
|
1656
|
+
return H3_NEED_MORE
|
|
1657
|
+
}
|
|
1658
|
+
if (phase === H3_PHASE_HEADERS || phase === H3_PHASE_SKIP) {
|
|
1659
|
+
// §4.1.1: the stream ended before a whole request.
|
|
1660
|
+
this.abandon(k, id, H3_REQUEST_INCOMPLETE)
|
|
1661
|
+
this.streamErrors = h3Count(this.streamErrors)
|
|
1662
|
+
return this.dropped(id)
|
|
1663
|
+
}
|
|
1664
|
+
if (this.contentLength[k] >= 0 && this.dataTotal[k] !== this.contentLength[k]) {
|
|
1665
|
+
// §4.1.2: the body is shorter than its content-length.
|
|
1666
|
+
this.abandon(k, id, H3_MESSAGE_ERROR)
|
|
1667
|
+
this.streamErrors = h3Count(this.streamErrors)
|
|
1668
|
+
return this.resetEvent(id, H3_MESSAGE_ERROR, false)
|
|
1669
|
+
}
|
|
1670
|
+
this.stream = id
|
|
1671
|
+
return H3_END
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/** Resets this side's half of request stream `id` with `code`, unless it is done already. */
|
|
1675
|
+
abandon(k: i32, id: i64, code: i64): void {
|
|
1676
|
+
if ((this.flags[k] & H3_FLAG_SEND_DONE) === 0) {
|
|
1677
|
+
this.quic.streamReset(id, code)
|
|
1678
|
+
}
|
|
1679
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/** A stream error (§8): the request is reset both ways with `code`, and the program told when it knew the request. */
|
|
1683
|
+
streamError(k: i32, id: i64, code: i64): i32 {
|
|
1684
|
+
this.streamErrors = h3Count(this.streamErrors)
|
|
1685
|
+
this.abandon(k, id, code)
|
|
1686
|
+
this.quic.streamStopSending(id, code)
|
|
1687
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
1688
|
+
if ((this.flags[k] & H3_FLAG_DELIVERED) !== 0) {
|
|
1689
|
+
return this.resetEvent(id, code, false)
|
|
1690
|
+
}
|
|
1691
|
+
return this.dropped(id)
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/** H3_DROPPED for request stream `id` with WebTransport on, which waits on such a stream; H3_NEED_MORE without. */
|
|
1695
|
+
dropped(id: i64): i32 {
|
|
1696
|
+
if (!this.webtransport) {
|
|
1697
|
+
return H3_NEED_MORE
|
|
1698
|
+
}
|
|
1699
|
+
this.stream = id
|
|
1700
|
+
return H3_DROPPED
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/** H3_RESET for `id` with `code`. */
|
|
1704
|
+
resetEvent(id: i64, code: i64, byPeer: boolean): i32 {
|
|
1705
|
+
this.stream = id
|
|
1706
|
+
this.errorCode = code
|
|
1707
|
+
this.resetByPeer = byPeer
|
|
1708
|
+
return H3_RESET
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* A request whose field section is past `maxFieldSectionSize`: answered
|
|
1713
|
+
* with a 431 and its stream asked to stop with H3_NO_ERROR (§4.2.2,
|
|
1714
|
+
* §4.1.2). The program never sees it.
|
|
1715
|
+
*/
|
|
1716
|
+
answerTooLarge(k: i32, id: i64): i32 {
|
|
1717
|
+
this.tooLarge = h3Count(this.tooLarge)
|
|
1718
|
+
const empty: u8[][] = this.nameList
|
|
1719
|
+
http3EmptyList(empty)
|
|
1720
|
+
if (this.writeHead(k, id, H3_STATUS_TOO_LARGE, empty, empty, true) !== 0) {
|
|
1721
|
+
this.abandon(k, id, H3_EXCESSIVE_LOAD)
|
|
1722
|
+
}
|
|
1723
|
+
this.quic.streamStopSending(id, H3_NO_ERROR)
|
|
1724
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
1725
|
+
return this.dropped(id)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* The stream's write side, when a write was held back: owed bytes are
|
|
1730
|
+
* written, and once the buffer has room again the program is told with
|
|
1731
|
+
* H3_WRITABLE. Answers that, or H3_NEED_MORE.
|
|
1732
|
+
*/
|
|
1733
|
+
writable(k: i32, id: i64): i32 {
|
|
1734
|
+
if ((this.flags[k] & H3_FLAG_BLOCKED) === 0 || (this.flags[k] & H3_FLAG_SEND_DONE) !== 0) {
|
|
1735
|
+
return H3_NEED_MORE
|
|
1736
|
+
}
|
|
1737
|
+
if (!this.drain(k, id) || this.quic.streams.slots[k].room() <= 0) {
|
|
1738
|
+
return H3_NEED_MORE
|
|
1739
|
+
}
|
|
1740
|
+
this.flags[k] = this.flags[k] & ~H3_FLAG_BLOCKED
|
|
1741
|
+
this.stream = id
|
|
1742
|
+
return H3_WRITABLE
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// ---- Writing ----------------------------------------------------------------------
|
|
1746
|
+
|
|
1747
|
+
/** The slot of request stream `id` when the program may write to it, else -1. */
|
|
1748
|
+
writeSlot(id: i64): i32 {
|
|
1749
|
+
if (this.state === H3_STATE_FAILED || !http3IsRequest(id)) {
|
|
1750
|
+
return -1
|
|
1751
|
+
}
|
|
1752
|
+
const k: i32 = this.quic.streams.slotOf(id)
|
|
1753
|
+
if (k < 0 || k >= toI32(this.slotId.length) || this.slotId[k] !== id) {
|
|
1754
|
+
return -1
|
|
1755
|
+
}
|
|
1756
|
+
const flags: i32 = this.flags[k]
|
|
1757
|
+
if ((flags & H3_FLAG_DELIVERED) === 0 || (flags & H3_FLAG_SEND_DONE) !== 0) {
|
|
1758
|
+
return -1
|
|
1759
|
+
}
|
|
1760
|
+
return this.quic.streams.slots[k].stopCode >= 0 ? -1 : k
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Writes the bytes owed on stream `id` before anything else — the rest of
|
|
1765
|
+
* a frame header, a filler's payload — as far as the send buffer takes
|
|
1766
|
+
* them. Answers whether none are owed now.
|
|
1767
|
+
*/
|
|
1768
|
+
drain(k: i32, id: i64): boolean {
|
|
1769
|
+
const base: i32 = k * H3_HEAD
|
|
1770
|
+
while (this.pendLen[k] > 0) {
|
|
1771
|
+
const n: i32 = this.quic.streamWrite(id, this.pend, base + this.pendAt[k], this.pendLen[k], false)
|
|
1772
|
+
if (n <= 0) {
|
|
1773
|
+
return false
|
|
1774
|
+
}
|
|
1775
|
+
this.pendAt[k] = this.pendAt[k] + n
|
|
1776
|
+
this.pendLen[k] = this.pendLen[k] - n
|
|
1777
|
+
}
|
|
1778
|
+
while (this.fillLeft[k] > 0) {
|
|
1779
|
+
const want: i32 = toI32(http3Min(this.fillLeft[k], toI64(toI32(this.zeros.length))))
|
|
1780
|
+
const n: i32 = this.quic.streamWrite(id, this.zeros, H3_ZERO, want, false)
|
|
1781
|
+
if (n <= 0) {
|
|
1782
|
+
return false
|
|
1783
|
+
}
|
|
1784
|
+
this.fillLeft[k] = this.fillLeft[k] - toI64(n)
|
|
1785
|
+
}
|
|
1786
|
+
return true
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
/** Queues `buf[0 .. n)`, a frame header, as the bytes owed first on slot `k`. */
|
|
1790
|
+
owe(k: i32, buf: u8[], n: i32): void {
|
|
1791
|
+
const base: i32 = k * H3_HEAD
|
|
1792
|
+
for (let j: i32 = 0; j < n && j < H3_HEAD && j < toI32(buf.length); j++) {
|
|
1793
|
+
this.pend[base + j] = buf[j]
|
|
1794
|
+
}
|
|
1795
|
+
this.pendAt[k] = 0
|
|
1796
|
+
this.pendLen[k] = n < H3_HEAD ? n : H3_HEAD
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* A frame of `need` bytes must go whole and the send buffer has less room:
|
|
1801
|
+
* the room left is filled with a reserved frame the client skips (§7.2.8),
|
|
1802
|
+
* so that QUIC names the stream again once acknowledgements free room.
|
|
1803
|
+
*/
|
|
1804
|
+
arm(k: i32, id: i64): void {
|
|
1805
|
+
const stream: QuicStream = this.quic.streams.slots[k]
|
|
1806
|
+
const room: i64 = stream.room()
|
|
1807
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1808
|
+
if (room > 0) {
|
|
1809
|
+
// A reserved frame of exactly `room` bytes: its type, its length in
|
|
1810
|
+
// the varint size that makes the sum come out (RFC 9000 §16 lets a
|
|
1811
|
+
// varint be longer than it needs), and zeros. One byte of room cannot
|
|
1812
|
+
// hold a frame, so it takes the type and owes the length.
|
|
1813
|
+
let size: i32 = 1
|
|
1814
|
+
if (room > 65) {
|
|
1815
|
+
size = 2
|
|
1816
|
+
}
|
|
1817
|
+
if (room > 16386) {
|
|
1818
|
+
size = 4
|
|
1819
|
+
}
|
|
1820
|
+
const payload: i64 = room > 1 ? room - toI64(1 + size) : toI64(0)
|
|
1821
|
+
this.scratch[0] = toU8(H3_FILLER_TYPE)
|
|
1822
|
+
const n: i32 = quicVarintPut(this.scratch, H3_ONE, payload, size)
|
|
1823
|
+
this.owe(k, this.scratch, n)
|
|
1824
|
+
this.fillLeft[k] = payload
|
|
1825
|
+
if (!this.drain(k, id)) {
|
|
1826
|
+
return
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// The room is filled: a write that takes nothing asks QUIC for the event.
|
|
1830
|
+
if (stream.room() === toI64(0)) {
|
|
1831
|
+
this.quic.streamWrite(id, this.zeros, H3_ZERO, H3_ONE, false)
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Encodes `:status` (unless it is 0, for trailers) and `names`/`values` into a HEADERS
|
|
1837
|
+
* frame and writes it whole to stream `id`, with the FIN when `fin`.
|
|
1838
|
+
* Answers 0, H3_AGAIN, or H3_TOO_LARGE.
|
|
1839
|
+
*/
|
|
1840
|
+
writeHead(k: i32, id: i64, status: i32, names: u8[][], values: u8[][], fin: boolean): i32 {
|
|
1841
|
+
const out: u8[] = this.encoded
|
|
1842
|
+
http3Empty(out)
|
|
1843
|
+
this.encoder.beginSection(out)
|
|
1844
|
+
let size: i64 = 0
|
|
1845
|
+
if (status > 0) {
|
|
1846
|
+
const ten: i32 = 10
|
|
1847
|
+
this.statusValue[0] = toU8(H3_DIGIT_ZERO + status / (ten * ten))
|
|
1848
|
+
this.statusValue[1] = toU8(H3_DIGIT_ZERO + ((status / ten) % ten))
|
|
1849
|
+
this.statusValue[2] = toU8(H3_DIGIT_ZERO + (status % ten))
|
|
1850
|
+
const nameLength: i32 = toI32(this.statusName.length)
|
|
1851
|
+
const valueLength: i32 = toI32(this.statusValue.length)
|
|
1852
|
+
this.encoder.encodeField(
|
|
1853
|
+
out,
|
|
1854
|
+
this.statusName,
|
|
1855
|
+
H3_ZERO,
|
|
1856
|
+
nameLength,
|
|
1857
|
+
this.statusValue,
|
|
1858
|
+
H3_ZERO,
|
|
1859
|
+
valueLength,
|
|
1860
|
+
false,
|
|
1861
|
+
true
|
|
1862
|
+
)
|
|
1863
|
+
size = toI64(nameLength + valueLength + QPACK_FIELD_OVERHEAD)
|
|
1864
|
+
}
|
|
1865
|
+
const n: i32 = toI32(names.length)
|
|
1866
|
+
for (let j: i32 = 0; j < n && j < toI32(names.length) && j < toI32(values.length); j++) {
|
|
1867
|
+
const name: u8[] = names[j]
|
|
1868
|
+
const value: u8[] = values[j]
|
|
1869
|
+
const nl: i32 = toI32(name.length)
|
|
1870
|
+
const vl: i32 = toI32(value.length)
|
|
1871
|
+
this.encoder.encodeField(out, name, H3_ZERO, nl, value, H3_ZERO, vl, httpFieldSensitive(name), true)
|
|
1872
|
+
size = size + toI64(nl + vl + QPACK_FIELD_OVERHEAD)
|
|
1873
|
+
}
|
|
1874
|
+
const limit: i64 = this.peer.maxFieldSectionSize
|
|
1875
|
+
const length: i64 = toI64(toI32(out.length))
|
|
1876
|
+
const total: i64 = toI64(h3FrameHeaderSize(H3_FRAME_HEADERS, length)) + length
|
|
1877
|
+
if ((limit >= 0 && size > limit) || total > toI64(this.quic.streams.bufferSize)) {
|
|
1878
|
+
return H3_TOO_LARGE
|
|
1879
|
+
}
|
|
1880
|
+
if (!this.drain(k, id)) {
|
|
1881
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1882
|
+
return H3_AGAIN
|
|
1883
|
+
}
|
|
1884
|
+
if (this.quic.streams.slots[k].room() < total) {
|
|
1885
|
+
this.arm(k, id)
|
|
1886
|
+
return H3_AGAIN
|
|
1887
|
+
}
|
|
1888
|
+
const hp: i32 = h3PutFrameHeader(
|
|
1889
|
+
this.scratch,
|
|
1890
|
+
H3_ZERO,
|
|
1891
|
+
toI32(this.scratch.length),
|
|
1892
|
+
H3_FRAME_HEADERS,
|
|
1893
|
+
length
|
|
1894
|
+
)
|
|
1895
|
+
this.quic.streamWrite(id, this.scratch, H3_ZERO, hp, false)
|
|
1896
|
+
this.quic.streamWrite(id, out, H3_ZERO, toI32(length), fin)
|
|
1897
|
+
this.flags[k] = this.flags[k] | H3_FLAG_HEADERS_SENT
|
|
1898
|
+
if (fin) {
|
|
1899
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
1900
|
+
}
|
|
1901
|
+
return 0
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Sends the response head on request stream `id`: `status` (200 to 599)
|
|
1906
|
+
* and the fields `names`/`values` (lowercase names, no pseudo-headers),
|
|
1907
|
+
* ending the stream when `endStream`. Answers 0; H3_CLOSED for a stream
|
|
1908
|
+
* that is not an open request; H3_INVALID for a second head, a status out
|
|
1909
|
+
* of range or a field `httpFieldsCheckOutgoing` refuses; H3_TOO_LARGE for a
|
|
1910
|
+
* section past the client's SETTINGS_MAX_FIELD_SECTION_SIZE; or H3_AGAIN.
|
|
1911
|
+
*/
|
|
1912
|
+
respond(id: i64, status: i32, names: u8[][], values: u8[][], endStream: boolean): i32 {
|
|
1913
|
+
const k: i32 = this.writeSlot(id)
|
|
1914
|
+
if (k < 0) {
|
|
1915
|
+
return H3_CLOSED
|
|
1916
|
+
}
|
|
1917
|
+
if (
|
|
1918
|
+
(this.flags[k] & H3_FLAG_HEADERS_SENT) !== 0 ||
|
|
1919
|
+
status < 200 ||
|
|
1920
|
+
status > 599 ||
|
|
1921
|
+
httpFieldsCheckOutgoing(names, values) !== HTTP_FIELDS_OK
|
|
1922
|
+
) {
|
|
1923
|
+
return H3_INVALID
|
|
1924
|
+
}
|
|
1925
|
+
return this.writeHead(k, id, status, names, values, endStream)
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Writes a trailer section on request stream `id` after its body, which
|
|
1930
|
+
* ends the stream. Answers as `respond` does; H3_INVALID before the head
|
|
1931
|
+
* or inside a DATA frame `writeData` has not finished.
|
|
1932
|
+
*/
|
|
1933
|
+
writeTrailers(id: i64, names: u8[][], values: u8[][]): i32 {
|
|
1934
|
+
const k: i32 = this.writeSlot(id)
|
|
1935
|
+
if (k < 0) {
|
|
1936
|
+
return H3_CLOSED
|
|
1937
|
+
}
|
|
1938
|
+
if (
|
|
1939
|
+
(this.flags[k] & H3_FLAG_HEADERS_SENT) === 0 ||
|
|
1940
|
+
this.dataLeft[k] > 0 ||
|
|
1941
|
+
httpFieldsCheckOutgoing(names, values) !== HTTP_FIELDS_OK
|
|
1942
|
+
) {
|
|
1943
|
+
return H3_INVALID
|
|
1944
|
+
}
|
|
1945
|
+
return this.writeHead(k, id, H3_ZERO, names, values, true)
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Writes up to `len` bytes of `buf` from `off` as the response body of
|
|
1950
|
+
* stream `id`, in DATA frames of at most `writeChunk`, and ends the stream
|
|
1951
|
+
* when `endStream` and every byte was taken. Answers how many bytes it
|
|
1952
|
+
* took — fewer than `len` only when the send buffer filled, and
|
|
1953
|
+
* H3_WRITABLE then names the stream once it has room — or H3_AGAIN when it
|
|
1954
|
+
* took none;
|
|
1955
|
+
* H3_CLOSED, or H3_INVALID before the head or when `endStream` would cut
|
|
1956
|
+
* short the DATA frame the last call started.
|
|
1957
|
+
*/
|
|
1958
|
+
writeData(id: i64, buf: u8[], off: i32, len: i32, endStream: boolean): i32 {
|
|
1959
|
+
h3CheckWindow("Http3Connection.writeData", buf, off, len)
|
|
1960
|
+
const k: i32 = this.writeSlot(id)
|
|
1961
|
+
if (k < 0) {
|
|
1962
|
+
return H3_CLOSED
|
|
1963
|
+
}
|
|
1964
|
+
if ((this.flags[k] & H3_FLAG_HEADERS_SENT) === 0 || (endStream && toI64(len) < this.dataLeft[k])) {
|
|
1965
|
+
return H3_INVALID
|
|
1966
|
+
}
|
|
1967
|
+
if (!this.drain(k, id)) {
|
|
1968
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1969
|
+
return H3_AGAIN
|
|
1970
|
+
}
|
|
1971
|
+
if (len === 0) {
|
|
1972
|
+
if (endStream && this.dataLeft[k] === 0) {
|
|
1973
|
+
this.quic.streamWrite(id, this.zeros, H3_ZERO, H3_ZERO, true)
|
|
1974
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
1975
|
+
}
|
|
1976
|
+
return 0
|
|
1977
|
+
}
|
|
1978
|
+
// DATA frame after DATA frame, until every byte is taken or the send buffer is full.
|
|
1979
|
+
let total: i32 = 0
|
|
1980
|
+
while (total < len) {
|
|
1981
|
+
if (this.dataLeft[k] === 0) {
|
|
1982
|
+
const rest: i32 = len - total
|
|
1983
|
+
const frame: i64 = toI64(rest < this.config.writeChunk ? rest : this.config.writeChunk)
|
|
1984
|
+
const n: i32 = h3PutFrameHeader(
|
|
1985
|
+
this.scratch,
|
|
1986
|
+
H3_ZERO,
|
|
1987
|
+
toI32(this.scratch.length),
|
|
1988
|
+
H3_FRAME_DATA,
|
|
1989
|
+
frame
|
|
1990
|
+
)
|
|
1991
|
+
this.owe(k, this.scratch, n)
|
|
1992
|
+
this.dataLeft[k] = frame
|
|
1993
|
+
if (!this.drain(k, id)) {
|
|
1994
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
1995
|
+
break
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const take: i32 = toI32(http3Min(toI64(len - total), this.dataLeft[k]))
|
|
1999
|
+
const fin: boolean = endStream && total + take === len && toI64(take) === this.dataLeft[k]
|
|
2000
|
+
const n: i32 = this.quic.streamWrite(id, buf, off + total, take, fin)
|
|
2001
|
+
if (n < 0) {
|
|
2002
|
+
return total > 0 ? total : H3_CLOSED
|
|
2003
|
+
}
|
|
2004
|
+
this.dataLeft[k] = this.dataLeft[k] - toI64(n)
|
|
2005
|
+
total = total + n
|
|
2006
|
+
if (n < take) {
|
|
2007
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
2008
|
+
break
|
|
2009
|
+
}
|
|
2010
|
+
if (fin) {
|
|
2011
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return total > 0 ? total : H3_AGAIN
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* Writes `buf[off .. off + len)` as one DATA frame on request stream `id`,
|
|
2019
|
+
* whole or not at all, with the FIN when `fin`: a capsule (RFC 9297 §3.2)
|
|
2020
|
+
* goes this way, so the client never reads half of one. Answers 0;
|
|
2021
|
+
* H3_AGAIN while the send buffer lacks room, and H3_WRITABLE then names the
|
|
2022
|
+
* stream; or as `writeData` refuses, and H3_TOO_LARGE for a frame past the
|
|
2023
|
+
* stream's buffer.
|
|
2024
|
+
*/
|
|
2025
|
+
writeDataWhole(id: i64, buf: u8[], off: i32, len: i32, fin: boolean): i32 {
|
|
2026
|
+
h3CheckWindow("Http3Connection.writeDataWhole", buf, off, len)
|
|
2027
|
+
const k: i32 = this.writeSlot(id)
|
|
2028
|
+
if (k < 0) {
|
|
2029
|
+
return H3_CLOSED
|
|
2030
|
+
}
|
|
2031
|
+
if ((this.flags[k] & H3_FLAG_HEADERS_SENT) === 0 || this.dataLeft[k] > 0) {
|
|
2032
|
+
return H3_INVALID
|
|
2033
|
+
}
|
|
2034
|
+
const length: i64 = toI64(len)
|
|
2035
|
+
const total: i64 = toI64(h3FrameHeaderSize(H3_FRAME_DATA, length)) + length
|
|
2036
|
+
if (total > toI64(this.quic.streams.bufferSize)) {
|
|
2037
|
+
return H3_TOO_LARGE
|
|
2038
|
+
}
|
|
2039
|
+
if (!this.drain(k, id)) {
|
|
2040
|
+
this.mark(k, H3_FLAG_BLOCKED)
|
|
2041
|
+
return H3_AGAIN
|
|
2042
|
+
}
|
|
2043
|
+
if (this.quic.streams.slots[k].room() < total) {
|
|
2044
|
+
this.arm(k, id)
|
|
2045
|
+
return H3_AGAIN
|
|
2046
|
+
}
|
|
2047
|
+
const hp: i32 = h3PutFrameHeader(this.scratch, H3_ZERO, toI32(this.scratch.length), H3_FRAME_DATA, length)
|
|
2048
|
+
this.quic.streamWrite(id, this.scratch, H3_ZERO, hp, false)
|
|
2049
|
+
this.quic.streamWrite(id, buf, off, len, fin)
|
|
2050
|
+
if (fin) {
|
|
2051
|
+
this.mark(k, H3_FLAG_SEND_DONE)
|
|
2052
|
+
}
|
|
2053
|
+
return 0
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Resets request stream `id` both ways with `code` — H3_REQUEST_CANCELLED,
|
|
2058
|
+
* or H3_REQUEST_REJECTED for one the program did not process (§4.1.1).
|
|
2059
|
+
* Answers 0 or H3_CLOSED.
|
|
2060
|
+
*/
|
|
2061
|
+
reset(id: i64, code: i64): i32 {
|
|
2062
|
+
const k: i32 = this.writeSlot(id)
|
|
2063
|
+
if (k < 0) {
|
|
2064
|
+
return H3_CLOSED
|
|
2065
|
+
}
|
|
2066
|
+
this.abandon(k, id, code)
|
|
2067
|
+
if (this.phase[k] !== H3_PHASE_DONE) {
|
|
2068
|
+
this.quic.streamStopSending(id, code)
|
|
2069
|
+
this.phase[k] = H3_PHASE_DISCARD
|
|
2070
|
+
}
|
|
2071
|
+
return 0
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Whether request stream `id` has not yet had its field section read:
|
|
2076
|
+
* not opened, opened by a later stream's frame (RFC 9000 §3.2), or still
|
|
2077
|
+
* waiting for its HEADERS. A WebTransport stream that names it as its
|
|
2078
|
+
* session may wait for it.
|
|
2079
|
+
*/
|
|
2080
|
+
awaitsRequest(id: i64): boolean {
|
|
2081
|
+
if (!http3IsRequest(id) || id < 0 || (this.goawayId >= 0 && id >= this.goawayId)) {
|
|
2082
|
+
return false
|
|
2083
|
+
}
|
|
2084
|
+
const k: i32 = this.quic.streams.slotOf(id)
|
|
2085
|
+
if (k < 0 || k >= toI32(this.slotId.length)) {
|
|
2086
|
+
// Not opened yet, and one the client's MAX_STREAMS lets it open now.
|
|
2087
|
+
const streams = this.quic.streams
|
|
2088
|
+
return id >= streams.peerBidiOpened << 2 && id < streams.peerBidiLimit << 2
|
|
2089
|
+
}
|
|
2090
|
+
if (this.slotId[k] !== id) {
|
|
2091
|
+
return true
|
|
2092
|
+
}
|
|
2093
|
+
return this.phase[k] === H3_PHASE_HEADERS && (this.flags[k] & H3_FLAG_WT) === 0
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/**
|
|
2097
|
+
* Stops reading request stream `id`: the client is asked to stop with
|
|
2098
|
+
* `code` and what it still sends is dropped unseen, whether or not this
|
|
2099
|
+
* side has finished its response — a refused WebTransport session's
|
|
2100
|
+
* stream, after its status. Answers 0, or H3_CLOSED for a stream that is
|
|
2101
|
+
* not a request here.
|
|
2102
|
+
*/
|
|
2103
|
+
discardRequest(id: i64, code: i64): i32 {
|
|
2104
|
+
if (this.state === H3_STATE_FAILED || !http3IsRequest(id)) {
|
|
2105
|
+
return H3_CLOSED
|
|
2106
|
+
}
|
|
2107
|
+
const k: i32 = this.quic.streams.slotOf(id)
|
|
2108
|
+
if (k < 0 || k >= toI32(this.slotId.length) || this.slotId[k] !== id) {
|
|
2109
|
+
return H3_CLOSED
|
|
2110
|
+
}
|
|
2111
|
+
this.stopReading(k, id, code)
|
|
2112
|
+
return 0
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/**
|
|
2116
|
+
* Sends GOAWAY (§5.2): the client's requests on streams below the ID it
|
|
2117
|
+
* names — every stream the client has opened so far — are processed, and
|
|
2118
|
+
* any it opens from there on is refused with H3_REQUEST_REJECTED. A second
|
|
2119
|
+
* call never raises the ID, and sends nothing when the ID has not fallen.
|
|
2120
|
+
* Answers 0, or H3_AGAIN before this side's control stream is open or while
|
|
2121
|
+
* it has no room for the frame.
|
|
2122
|
+
*/
|
|
2123
|
+
goaway(): i32 {
|
|
2124
|
+
if (this.ownControl < 0 || this.state === H3_STATE_FAILED) {
|
|
2125
|
+
return H3_AGAIN
|
|
2126
|
+
}
|
|
2127
|
+
let id: i64 = this.quic.streams.peerBidiOpened << 2
|
|
2128
|
+
if (this.goawayId >= 0 && id > this.goawayId) {
|
|
2129
|
+
id = this.goawayId
|
|
2130
|
+
}
|
|
2131
|
+
if (id === this.goawayId) {
|
|
2132
|
+
// The client has this ID already: nothing new to say.
|
|
2133
|
+
return 0
|
|
2134
|
+
}
|
|
2135
|
+
const n: i32 = h3PutIdFrame(this.scratch, H3_ZERO, toI32(this.scratch.length), H3_FRAME_GOAWAY, id)
|
|
2136
|
+
const k: i32 = this.quic.streams.slotOf(this.ownControl)
|
|
2137
|
+
// Whole or not at all: half a GOAWAY on the control stream would be a frame error.
|
|
2138
|
+
if (k < 0 || k >= toI32(this.quic.streams.slots.length) || this.quic.streams.slots[k].room() < toI64(n)) {
|
|
2139
|
+
return H3_AGAIN
|
|
2140
|
+
}
|
|
2141
|
+
this.quic.streamWrite(this.ownControl, this.scratch, H3_ZERO, n, false)
|
|
2142
|
+
this.goawayId = id
|
|
2143
|
+
return 0
|
|
2144
|
+
}
|
|
2145
|
+
}
|