@amritk/nish-x86_64-linux 0.15.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 +58 -40
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +67 -10
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +72 -26
- package/runtime/runtime-net.c +72 -19
- package/runtime/runtime-os.c +127 -45
- package/runtime/runtime-parallel.c +5 -1
- package/runtime/runtime-wasm.c +26 -2
- package/runtime/runtime.c +86 -14
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +1 -3
- package/std/README.md +192 -36
- package/std/crypto/aes.ts +104 -21
- package/std/crypto/base64url.ts +43 -17
- package/std/crypto/chacha20poly1305.ts +52 -31
- package/std/crypto/ct.ts +14 -3
- package/std/crypto/hkdf.ts +385 -8
- package/std/crypto/hmac.ts +325 -9
- package/std/crypto/p256.ts +153 -27
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +15 -3
- package/std/crypto/sha512.ts +16 -2
- package/std/crypto/x25519.ts +90 -37
- package/std/crypto/x509.ts +265 -65
- 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/http2.ts
ADDED
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/http2` — the server side of an HTTP/2 connection (RFC 9113):
|
|
3
|
+
* the connection preface, SETTINGS and their acknowledgement, the stream
|
|
4
|
+
* state machine of §5.1, flow control on the connection and on every stream
|
|
5
|
+
* with WINDOW_UPDATE, PING, RST_STREAM, GOAWAY with the last stream
|
|
6
|
+
* identifier, header blocks across CONTINUATION frames decoded by
|
|
7
|
+
* `nish/net/hpack`, and extended CONNECT (RFC 8441).
|
|
8
|
+
*
|
|
9
|
+
* It is sans-IO: bytes in, events and bytes out, and no socket anywhere.
|
|
10
|
+
* `nish/net/http2-tls` runs it over TLS with ALPN `h2`.
|
|
11
|
+
*
|
|
12
|
+
* import { Http2Config, Http2Connection, H2_NEED_MORE, H2_REQUEST, H2_DATA } from "nish/net/http2";
|
|
13
|
+
*
|
|
14
|
+
* const conn = new Http2Connection(new Http2Config());
|
|
15
|
+
* // send conn.output[conn.outputStart .. conn.outputEnd), then conn.consume(sent)
|
|
16
|
+
* conn.feed(received, 0, n);
|
|
17
|
+
* let event: i32 = conn.next();
|
|
18
|
+
* while (event !== H2_NEED_MORE && event !== H2_ERROR) {
|
|
19
|
+
* // H2_REQUEST: conn.stream, conn.fields (method, path, get("accept")), conn.endStream
|
|
20
|
+
* // H2_DATA: conn.data[conn.dataStart .. conn.dataStart + conn.dataLength), conn.endStream
|
|
21
|
+
* event = conn.next();
|
|
22
|
+
* }
|
|
23
|
+
* conn.respond(stream, 200, names, values, false);
|
|
24
|
+
* conn.writeData(stream, body, 0, n, true);
|
|
25
|
+
*
|
|
26
|
+
* **Events.** `next()` reads the frames `feed` handed it and answers the
|
|
27
|
+
* first that means something to the program: `H2_REQUEST` (a stream opened
|
|
28
|
+
* with a request's header section; `fields` holds it), `H2_DATA` (some of a
|
|
29
|
+
* request body, a window onto the input buffer that stays valid until the
|
|
30
|
+
* next `feed`), `H2_TRAILERS`, `H2_RESET` (the stream is gone: the peer reset
|
|
31
|
+
* it, or broke a rule that resets it — `errorCode` says which), `H2_WINDOW`
|
|
32
|
+
* (a send window that held `writeData` back has opened: `stream`, or 0 for the
|
|
33
|
+
* connection's), `H2_GOAWAY` (the peer is going away; `lastStreamId`), and
|
|
34
|
+
* `H2_ERROR`, final: the connection broke a rule, a GOAWAY with `errorCode`
|
|
35
|
+
* is queued, and every later call answers it again. `H2_NEED_MORE` asks for
|
|
36
|
+
* more input, or for the output to drain: a frame is read only while the
|
|
37
|
+
* output has room for whatever it makes the connection answer, so a peer
|
|
38
|
+
* that sends PINGs or SETTINGS and never reads the acknowledgements stops
|
|
39
|
+
* being read rather than growing a queue. Bodies move a frame at a time and
|
|
40
|
+
* nothing buffers a whole one.
|
|
41
|
+
*
|
|
42
|
+
* **What the connection refuses**, as RFC 9113 classes it. A connection error
|
|
43
|
+
* sends GOAWAY and ends the connection: a bad preface or a first frame that is
|
|
44
|
+
* not SETTINGS (PROTOCOL_ERROR, §3.4); every refusal of
|
|
45
|
+
* `nish/net/http2-frame`; a frame other than CONTINUATION inside a header
|
|
46
|
+
* block, or a CONTINUATION outside one (§6.10); a stream identifier that is
|
|
47
|
+
* even or does not increase (§5.1.1); DATA, WINDOW_UPDATE or RST_STREAM on an
|
|
48
|
+
* idle stream, and a PUSH_PROMISE from a client (PROTOCOL_ERROR); HEADERS on
|
|
49
|
+
* a closed stream (STREAM_CLOSED); a header block HPACK cannot decode
|
|
50
|
+
* (COMPRESSION_ERROR, RFC 7541 §2.3.2); DATA past the connection window, or a
|
|
51
|
+
* window pushed past 2^31 − 1 (FLOW_CONTROL_ERROR, §6.9); a setting
|
|
52
|
+
* `http2SettingError` refuses; a header block longer than `maxHeaderBlock`
|
|
53
|
+
* or cut into more than `H2_MAX_FRAGMENTS` frames, and more streams reset by
|
|
54
|
+
* the peer than `resetBudget` allows (ENHANCE_YOUR_CALM, §10.5). A stream
|
|
55
|
+
* error resets one stream and the connection lives: a malformed request — any
|
|
56
|
+
* refusal of `nish/net/http-fields`, trailers without END_STREAM, a body that
|
|
57
|
+
* disagrees with `content-length` (PROTOCOL_ERROR, §8.1.1); DATA or HEADERS
|
|
58
|
+
* after the peer ended the stream (STREAM_CLOSED, §5.1); DATA past the
|
|
59
|
+
* stream's window, or its window pushed past 2^31 − 1 (FLOW_CONTROL_ERROR);
|
|
60
|
+
* a stream past `maxStreams` (REFUSED_STREAM, §5.1.2); a stream that depends
|
|
61
|
+
* on itself and a PRIORITY of the wrong length. A request whose header list
|
|
62
|
+
* is longer than `maxHeaderListSize` is answered with a 431 and never reaches
|
|
63
|
+
* the program (§10.5.1).
|
|
64
|
+
*
|
|
65
|
+
* **Caps are start-up numbers**, all in `Http2Config`: the streams open at
|
|
66
|
+
* once, the windows, the largest frame and header block accepted, the header
|
|
67
|
+
* list, the output buffer and the reset budget. Every buffer and every
|
|
68
|
+
* stream slot is allocated by the constructor; reading DATA, writing DATA and
|
|
69
|
+
* every control frame allocate nothing. A header block does allocate, through
|
|
70
|
+
* `HpackDecoder` (`docs/security/http2.md`, H2-1).
|
|
71
|
+
*
|
|
72
|
+
* The server sends no PUSH_PROMISE and no PRIORITY, and reads PRIORITY only to
|
|
73
|
+
* check it: RFC 9113 §5.3.2 deprecates the scheme. Every field it sends is a
|
|
74
|
+
* literal without indexing, so its encoder's table stays empty and the peer's
|
|
75
|
+
* SETTINGS_HEADER_TABLE_SIZE only ever asks it for a size update; a field
|
|
76
|
+
* `httpFieldSensitive` names (`set-cookie`, `authorization`, …) is a
|
|
77
|
+
* never-indexed literal, which no intermediary may index either (RFC 7541
|
|
78
|
+
* §7.1.3).
|
|
79
|
+
*
|
|
80
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
81
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
82
|
+
*
|
|
83
|
+
* Written from RFC 9113 and RFC 8441, not ported from another implementation.
|
|
84
|
+
*/
|
|
85
|
+
import {
|
|
86
|
+
HPACK_DEFAULT_TABLE_SIZE,
|
|
87
|
+
HPACK_INDEX_NEVER,
|
|
88
|
+
HPACK_INDEX_WITHOUT,
|
|
89
|
+
HPACK_LIST_TOO_LARGE,
|
|
90
|
+
HpackDecoder,
|
|
91
|
+
HpackEncoder,
|
|
92
|
+
hpackHuffmanLength,
|
|
93
|
+
} from "nish/net/hpack"
|
|
94
|
+
import {
|
|
95
|
+
HTTP_FIELDS_OK,
|
|
96
|
+
HttpFields,
|
|
97
|
+
httpFieldBytes,
|
|
98
|
+
httpFieldSensitive,
|
|
99
|
+
httpFieldsCheckOutgoing,
|
|
100
|
+
} from "nish/net/http-fields"
|
|
101
|
+
import {
|
|
102
|
+
H2_COMPRESSION_ERROR,
|
|
103
|
+
H2_DEFAULT_MAX_FRAME,
|
|
104
|
+
H2_DEFAULT_WINDOW,
|
|
105
|
+
H2_ENHANCE_YOUR_CALM,
|
|
106
|
+
H2_FLAG_ACK,
|
|
107
|
+
H2_FLAG_END_HEADERS,
|
|
108
|
+
H2_FLAG_END_STREAM,
|
|
109
|
+
H2_FLOW_CONTROL_ERROR,
|
|
110
|
+
H2_FRAME_CONTINUATION,
|
|
111
|
+
H2_FRAME_DATA,
|
|
112
|
+
H2_FRAME_GOAWAY,
|
|
113
|
+
H2_FRAME_HEADER_SIZE,
|
|
114
|
+
H2_FRAME_HEADERS,
|
|
115
|
+
H2_FRAME_PING,
|
|
116
|
+
H2_FRAME_PRIORITY,
|
|
117
|
+
H2_FRAME_PUSH_PROMISE,
|
|
118
|
+
H2_FRAME_RST_STREAM,
|
|
119
|
+
H2_FRAME_SETTINGS,
|
|
120
|
+
H2_FRAME_SIZE_ERROR,
|
|
121
|
+
H2_FRAME_WINDOW_UPDATE,
|
|
122
|
+
H2_MAX_FRAME_LIMIT,
|
|
123
|
+
H2_MAX_WINDOW,
|
|
124
|
+
H2_NO_ERROR,
|
|
125
|
+
H2_PROTOCOL_ERROR,
|
|
126
|
+
H2_REFUSED_STREAM,
|
|
127
|
+
H2_SETTINGS_ENABLE_CONNECT_PROTOCOL,
|
|
128
|
+
H2_SETTINGS_HEADER_TABLE_SIZE,
|
|
129
|
+
H2_SETTINGS_INITIAL_WINDOW_SIZE,
|
|
130
|
+
H2_SETTINGS_MAX_CONCURRENT_STREAMS,
|
|
131
|
+
H2_SETTINGS_MAX_FRAME_SIZE,
|
|
132
|
+
H2_SETTINGS_MAX_HEADER_LIST_SIZE,
|
|
133
|
+
H2_STREAM_CLOSED,
|
|
134
|
+
Http2Frame,
|
|
135
|
+
http2ParseFrame,
|
|
136
|
+
http2ReadHeader,
|
|
137
|
+
http2SettingCount,
|
|
138
|
+
http2SettingError,
|
|
139
|
+
http2SettingId,
|
|
140
|
+
http2SettingValue,
|
|
141
|
+
http2WriteContinuation,
|
|
142
|
+
http2WriteData,
|
|
143
|
+
http2WriteGoaway,
|
|
144
|
+
http2WriteHeaders,
|
|
145
|
+
http2WritePing,
|
|
146
|
+
http2WriteRstStream,
|
|
147
|
+
http2WriteSettings,
|
|
148
|
+
http2WriteSettingsAck,
|
|
149
|
+
http2WriteWindowUpdate,
|
|
150
|
+
} from "nish/net/http2-frame"
|
|
151
|
+
|
|
152
|
+
/** `next` needs more input, or the output to drain. */
|
|
153
|
+
export const H2_NEED_MORE: i32 = 0
|
|
154
|
+
/** A stream opened with a request: `stream`, `fields`, `endStream`. */
|
|
155
|
+
export const H2_REQUEST: i32 = 1
|
|
156
|
+
/** Request body bytes: `stream`, `data[dataStart .. dataStart + dataLength)`, `endStream`. */
|
|
157
|
+
export const H2_DATA: i32 = 2
|
|
158
|
+
/** A trailer section, which ends the request: `stream`, `fields`. */
|
|
159
|
+
export const H2_TRAILERS: i32 = 3
|
|
160
|
+
/** The stream is gone, reset by the peer or for a rule it broke: `stream`, `errorCode`, `resetByPeer`. */
|
|
161
|
+
export const H2_RESET: i32 = 4
|
|
162
|
+
/** A send window that held `writeData` back has opened: `stream`, 0 for the connection's. */
|
|
163
|
+
export const H2_WINDOW: i32 = 5
|
|
164
|
+
/** The peer sent GOAWAY: `lastStreamId`, `errorCode`. */
|
|
165
|
+
export const H2_GOAWAY: i32 = 6
|
|
166
|
+
/** The connection failed with `errorCode` and a GOAWAY is queued. Final. */
|
|
167
|
+
export const H2_ERROR: i32 = 7
|
|
168
|
+
|
|
169
|
+
/** What `writeData` answers when nothing can be sent yet: Linux's EAGAIN, as `nish:net` spells it. */
|
|
170
|
+
export const H2_AGAIN: i32 = -11
|
|
171
|
+
/** What a write answers for fields or a status it may not send: Linux's EINVAL. */
|
|
172
|
+
export const H2_INVALID: i32 = -22
|
|
173
|
+
/** What a write answers for a stream it may not write to: Linux's EPIPE. */
|
|
174
|
+
export const H2_CLOSED: i32 = -32
|
|
175
|
+
|
|
176
|
+
/** The connection preface every client sends first (§3.4). */
|
|
177
|
+
const H2_PREFACE: string = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
|
178
|
+
|
|
179
|
+
/** The preface's length. */
|
|
180
|
+
const H2_PREFACE_SIZE: i32 = 24
|
|
181
|
+
|
|
182
|
+
/** How many frames one header block may be cut into. */
|
|
183
|
+
export const H2_MAX_FRAGMENTS: i32 = 64
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Output kept free of DATA and HEADERS, so the control frames a read frame
|
|
187
|
+
* can make the connection send — two WINDOW_UPDATEs, a RST_STREAM, a 431, a
|
|
188
|
+
* PING or SETTINGS acknowledgement — and the GOAWAY after them always fit.
|
|
189
|
+
*/
|
|
190
|
+
const H2_CONTROL_RESERVE: i32 = 128
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* How many stream identifiers the connection remembers having reset, and
|
|
194
|
+
* having closed, beyond `maxStreams`: enough for the program to reset every
|
|
195
|
+
* open stream at once and still ignore what the peer had in flight for each.
|
|
196
|
+
*/
|
|
197
|
+
const H2_RECENT_RESETS: i32 = 16
|
|
198
|
+
|
|
199
|
+
/** The connection's states. */
|
|
200
|
+
const H2_STATE_PREFACE: i32 = 0
|
|
201
|
+
const H2_STATE_SETTINGS: i32 = 1
|
|
202
|
+
const H2_STATE_OPEN: i32 = 2
|
|
203
|
+
const H2_STATE_FAILED: i32 = 3
|
|
204
|
+
|
|
205
|
+
/** A typed zero: a bare literal is an `f64` under `--number-mode f64`. */
|
|
206
|
+
const H2_ZERO: i32 = 0
|
|
207
|
+
|
|
208
|
+
/** The character `0`, typed, which a status digit is added to. */
|
|
209
|
+
const H2_DIGIT_ZERO: i32 = 48
|
|
210
|
+
|
|
211
|
+
/** 431 Request Header Fields Too Large (RFC 6585 §5), typed. */
|
|
212
|
+
const H2_STATUS_TOO_LARGE: i32 = 431
|
|
213
|
+
|
|
214
|
+
/** A typed -1, for no padding. */
|
|
215
|
+
const H2_NO_PAD: i32 = -1
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The connection's caps, fixed when it is made. The defaults are RFC 9113's
|
|
219
|
+
* where it has one; each is advertised in the server's SETTINGS where a
|
|
220
|
+
* setting names it.
|
|
221
|
+
*/
|
|
222
|
+
export class Http2Config {
|
|
223
|
+
/** SETTINGS_MAX_CONCURRENT_STREAMS, and the stream slots the connection allocates. */
|
|
224
|
+
maxStreams: i32 = 100
|
|
225
|
+
/** SETTINGS_INITIAL_WINDOW_SIZE: what each stream may send before the server credits it. */
|
|
226
|
+
initialWindowSize: i32 = 65535
|
|
227
|
+
/** The connection's receive window, raised from 65,535 by a WINDOW_UPDATE at the start. */
|
|
228
|
+
connectionWindowSize: i32 = 1048576
|
|
229
|
+
/** SETTINGS_MAX_FRAME_SIZE: the largest frame payload read, which sizes the input buffer. */
|
|
230
|
+
maxFrameSize: i32 = 16384
|
|
231
|
+
/** SETTINGS_MAX_HEADER_LIST_SIZE: past it a request is answered with a 431. */
|
|
232
|
+
maxHeaderListSize: i32 = 16384
|
|
233
|
+
/** The largest header block, compressed, across HEADERS and CONTINUATION. */
|
|
234
|
+
maxHeaderBlock: i32 = 16384
|
|
235
|
+
/** SETTINGS_HEADER_TABLE_SIZE: the HPACK decoder's table. */
|
|
236
|
+
headerTableSize: i32 = 4096
|
|
237
|
+
/** SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC 8441): whether `:protocol` is accepted. */
|
|
238
|
+
enableConnectProtocol: boolean = false
|
|
239
|
+
/** The output buffer, in bytes. */
|
|
240
|
+
outputSize: i32 = 65536
|
|
241
|
+
/** How many streams the peer may reset before finishing, net of the ones it lets finish. */
|
|
242
|
+
resetBudget: i32 = 100
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** One stream's slot. `id` is 0 while the slot is free. */
|
|
246
|
+
export class Http2Stream {
|
|
247
|
+
/** What the server may still send on the stream. */
|
|
248
|
+
sendWindow: i64 = 0
|
|
249
|
+
/** What the peer may still send on it. */
|
|
250
|
+
recvWindow: i64 = 0
|
|
251
|
+
/** The request's `content-length`, or -1. */
|
|
252
|
+
contentLength: i64 = -1
|
|
253
|
+
/** The body bytes received. */
|
|
254
|
+
received: i64 = 0
|
|
255
|
+
id: i32 = 0
|
|
256
|
+
/** What the program has been handed since the last WINDOW_UPDATE for it. */
|
|
257
|
+
recvUnacked: i32 = 0
|
|
258
|
+
/** The peer sent END_STREAM: half-closed (remote). */
|
|
259
|
+
remoteEnded: boolean = false
|
|
260
|
+
/** The server sent END_STREAM: half-closed (local). */
|
|
261
|
+
localEnded: boolean = false
|
|
262
|
+
/** A final (non-1xx) response head was sent. */
|
|
263
|
+
responded: boolean = false
|
|
264
|
+
/** `writeData` was held back by this stream's window. */
|
|
265
|
+
blocked: boolean = false
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Whether `buf[off ..]` holds the preface. */
|
|
269
|
+
const http2IsPreface = (buf: u8[], off: i32): boolean => {
|
|
270
|
+
for (let k: i32 = 0; k < H2_PREFACE_SIZE && off + k < toI32(buf.length); k++) {
|
|
271
|
+
if (toI32(buf[off + k]) !== toI32(H2_PREFACE.charCodeAt(k))) {
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Moves `buf[start, end)` to the front of `buf` and answers where it now ends. */
|
|
279
|
+
const http2Compact = (buf: u8[], start: i32, end: i32): i32 => {
|
|
280
|
+
const keep: i32 = end - start
|
|
281
|
+
if (start > 0) {
|
|
282
|
+
for (let k: i32 = 0; k < keep && k < toI32(buf.length) && start + k < toI32(buf.length); k++) {
|
|
283
|
+
buf[k] = buf[start + k]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return keep
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Writes `id` at `at` in the ring `ring` and answers where the next one goes. */
|
|
290
|
+
const http2Remember = (ring: i32[], at: i32, id: i32): i32 => {
|
|
291
|
+
const slot: i32 = at % toI32(ring.length)
|
|
292
|
+
if (slot >= 0 && slot < toI32(ring.length)) {
|
|
293
|
+
ring[slot] = id
|
|
294
|
+
}
|
|
295
|
+
return slot + 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Panics unless `[off, off + len)` lies inside `buf`. */
|
|
299
|
+
const http2CheckWindow = (what: string, buf: u8[], off: i32, len: i32): void => {
|
|
300
|
+
if (off < 0 || len < 0 || toI64(off) + toI64(len) > toI64(buf.length)) {
|
|
301
|
+
panic(`${what}: the window [${off}, ${off} + ${len}) is outside a buffer of ${buf.length} bytes`)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Panics unless `value` lies in `[low, high]`: a cap out of range is the program's mistake. */
|
|
306
|
+
const http2CheckCap = (what: string, value: i32, low: i32, high: i32): void => {
|
|
307
|
+
if (value < low || value > high) {
|
|
308
|
+
panic(`Http2Connection: ${what} of ${value}, outside ${low} to ${high}`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* One HTTP/2 connection, server side. The fields after the buffers are the
|
|
314
|
+
* last event's; the module comment says which event sets which.
|
|
315
|
+
*/
|
|
316
|
+
export class Http2Connection {
|
|
317
|
+
// Pointers and 64-bit fields first, then 32-bit ones, then flags: the
|
|
318
|
+
// order the layout packs without padding.
|
|
319
|
+
config: Http2Config
|
|
320
|
+
frame: Http2Frame
|
|
321
|
+
/** Bytes fed and not read yet are `input[inputStart .. inputEnd)`. */
|
|
322
|
+
input: u8[]
|
|
323
|
+
/** Bytes to send are `output[outputStart .. outputEnd)`. */
|
|
324
|
+
output: u8[]
|
|
325
|
+
/** The header block being gathered across CONTINUATION frames. */
|
|
326
|
+
block: u8[]
|
|
327
|
+
/** Where a response's header block is encoded, emptied and reused. */
|
|
328
|
+
encoded: u8[]
|
|
329
|
+
decoder: HpackDecoder
|
|
330
|
+
encoder: HpackEncoder
|
|
331
|
+
/** The last request's or trailers' fields. */
|
|
332
|
+
fields: HttpFields
|
|
333
|
+
streams: Http2Stream[]
|
|
334
|
+
/** The identifiers the server reset most recently, a ring. */
|
|
335
|
+
resets: i32[]
|
|
336
|
+
/** The identifiers of the streams closed most recently, however they closed, a ring. */
|
|
337
|
+
closed: i32[]
|
|
338
|
+
/** The bytes of `:status` and of a three-digit status, reused for every response. */
|
|
339
|
+
statusName: u8[]
|
|
340
|
+
statusValue: u8[]
|
|
341
|
+
/** GOAWAY's debug data, which the server never sends: one empty array, made once. */
|
|
342
|
+
noDebug: u8[]
|
|
343
|
+
/** The server's SETTINGS, made once and sent by every `restart`. */
|
|
344
|
+
settingIds: i32[]
|
|
345
|
+
settingValues: i64[]
|
|
346
|
+
/** The peer's SETTINGS_INITIAL_WINDOW_SIZE. */
|
|
347
|
+
peerInitialWindow: i64 = 65535
|
|
348
|
+
/** The peer's SETTINGS_MAX_HEADER_LIST_SIZE, advisory; -1 until it sends one. */
|
|
349
|
+
peerMaxHeaderList: i64 = -1
|
|
350
|
+
/** The window a new stream starts with on the receiving side. */
|
|
351
|
+
localInitialWindow: i64 = 65535
|
|
352
|
+
/** The connection's send window. */
|
|
353
|
+
sendWindow: i64 = 65535
|
|
354
|
+
/** The connection's receive window, and what has been handed on since it was last credited. */
|
|
355
|
+
recvWindow: i64 = 65535
|
|
356
|
+
recvUnacked: i64 = 0
|
|
357
|
+
/** H2_DATA's bytes: the input buffer, valid until the next `feed`. */
|
|
358
|
+
data: u8[]
|
|
359
|
+
/** H2_RESET's, H2_GOAWAY's and H2_ERROR's code. */
|
|
360
|
+
errorCode: i64 = 0
|
|
361
|
+
inputStart: i32 = 0
|
|
362
|
+
inputEnd: i32 = 0
|
|
363
|
+
outputStart: i32 = 0
|
|
364
|
+
outputEnd: i32 = 0
|
|
365
|
+
blockLength: i32 = 0
|
|
366
|
+
/** The stream the open header block is on, or 0 when none is open. */
|
|
367
|
+
blockStream: i32 = 0
|
|
368
|
+
/** A stream error the HEADERS that opened the block carried. */
|
|
369
|
+
blockError: i32 = 0
|
|
370
|
+
blockFragments: i32 = 0
|
|
371
|
+
resetAt: i32 = 0
|
|
372
|
+
closedAt: i32 = 0
|
|
373
|
+
state: i32 = 0
|
|
374
|
+
/** The highest stream identifier the peer has opened. */
|
|
375
|
+
lastPeerStream: i32 = 0
|
|
376
|
+
/** Streams with a slot. */
|
|
377
|
+
active: i32 = 0
|
|
378
|
+
/** The peer's SETTINGS_MAX_FRAME_SIZE. */
|
|
379
|
+
peerMaxFrameSize: i32 = 16384
|
|
380
|
+
/** What is left of `config.resetBudget`. */
|
|
381
|
+
resetCredit: i32 = 0
|
|
382
|
+
/** The last event's stream. */
|
|
383
|
+
stream: i32 = 0
|
|
384
|
+
/** H2_DATA's window onto `data`. */
|
|
385
|
+
dataStart: i32 = 0
|
|
386
|
+
dataLength: i32 = 0
|
|
387
|
+
/** H2_GOAWAY's last stream identifier. */
|
|
388
|
+
lastStreamId: i32 = 0
|
|
389
|
+
/** The last stream identifier the server's first GOAWAY named, which no later one may raise (§6.8). */
|
|
390
|
+
sentLast: i32 = 0
|
|
391
|
+
blockEndStream: boolean = false
|
|
392
|
+
/** Whether the peer has acknowledged the server's SETTINGS. */
|
|
393
|
+
settingsAcked: boolean = false
|
|
394
|
+
/** `writeData` was held back by the connection's window. */
|
|
395
|
+
sendBlocked: boolean = false
|
|
396
|
+
goawaySent: boolean = false
|
|
397
|
+
goawayReceived: boolean = false
|
|
398
|
+
/** The peer's byte stream has ended (`endInput`). */
|
|
399
|
+
inputEnded: boolean = false
|
|
400
|
+
/** Whether the last H2_REQUEST, H2_DATA or H2_TRAILERS ended its stream. */
|
|
401
|
+
endStream: boolean = false
|
|
402
|
+
/** Whether H2_RESET came from the peer's RST_STREAM rather than a rule its stream broke. */
|
|
403
|
+
resetByPeer: boolean = false
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* A connection under `config`, with its SETTINGS — and the WINDOW_UPDATE
|
|
407
|
+
* that raises the connection's window, when `connectionWindowSize` asks
|
|
408
|
+
* for one — already in `output`, since a server's preface is its SETTINGS
|
|
409
|
+
* (§3.4). A cap outside what RFC 9113 allows panics.
|
|
410
|
+
*/
|
|
411
|
+
constructor(config: Http2Config) {
|
|
412
|
+
http2CheckCap("maxStreams", config.maxStreams, 1, 65536)
|
|
413
|
+
http2CheckCap("initialWindowSize", config.initialWindowSize, 1, H2_MAX_WINDOW)
|
|
414
|
+
http2CheckCap("connectionWindowSize", config.connectionWindowSize, H2_DEFAULT_WINDOW, H2_MAX_WINDOW)
|
|
415
|
+
http2CheckCap("maxFrameSize", config.maxFrameSize, H2_DEFAULT_MAX_FRAME, H2_MAX_FRAME_LIMIT)
|
|
416
|
+
http2CheckCap("maxHeaderListSize", config.maxHeaderListSize, 0, H2_MAX_WINDOW)
|
|
417
|
+
http2CheckCap("maxHeaderBlock", config.maxHeaderBlock, 1, H2_MAX_WINDOW)
|
|
418
|
+
http2CheckCap("headerTableSize", config.headerTableSize, 0, H2_MAX_WINDOW)
|
|
419
|
+
http2CheckCap("outputSize", config.outputSize, 1024, H2_MAX_WINDOW)
|
|
420
|
+
http2CheckCap("resetBudget", config.resetBudget, 0, H2_MAX_WINDOW)
|
|
421
|
+
this.config = config
|
|
422
|
+
this.frame = new Http2Frame()
|
|
423
|
+
this.input = new Array<u8>(config.maxFrameSize + H2_FRAME_HEADER_SIZE)
|
|
424
|
+
this.output = new Array<u8>(config.outputSize)
|
|
425
|
+
this.block = new Array<u8>(config.maxHeaderBlock)
|
|
426
|
+
this.encoded = []
|
|
427
|
+
this.decoder = new HpackDecoder(HPACK_DEFAULT_TABLE_SIZE, config.maxHeaderListSize)
|
|
428
|
+
this.encoder = new HpackEncoder(HPACK_DEFAULT_TABLE_SIZE)
|
|
429
|
+
this.fields = new HttpFields()
|
|
430
|
+
this.streams = []
|
|
431
|
+
for (let k: i32 = 0; k < config.maxStreams; k++) {
|
|
432
|
+
this.streams.push(new Http2Stream())
|
|
433
|
+
}
|
|
434
|
+
this.resets = new Array<i32>(config.maxStreams + H2_RECENT_RESETS)
|
|
435
|
+
this.closed = new Array<i32>(config.maxStreams + H2_RECENT_RESETS)
|
|
436
|
+
this.statusName = httpFieldBytes(":status")
|
|
437
|
+
this.noDebug = []
|
|
438
|
+
this.statusValue = [48, 48, 48]
|
|
439
|
+
this.data = this.input
|
|
440
|
+
this.settingIds = [
|
|
441
|
+
H2_SETTINGS_HEADER_TABLE_SIZE,
|
|
442
|
+
H2_SETTINGS_MAX_CONCURRENT_STREAMS,
|
|
443
|
+
H2_SETTINGS_INITIAL_WINDOW_SIZE,
|
|
444
|
+
H2_SETTINGS_MAX_FRAME_SIZE,
|
|
445
|
+
H2_SETTINGS_MAX_HEADER_LIST_SIZE,
|
|
446
|
+
]
|
|
447
|
+
this.settingValues = [
|
|
448
|
+
toI64(config.headerTableSize),
|
|
449
|
+
toI64(config.maxStreams),
|
|
450
|
+
toI64(config.initialWindowSize),
|
|
451
|
+
toI64(config.maxFrameSize),
|
|
452
|
+
toI64(config.maxHeaderListSize),
|
|
453
|
+
]
|
|
454
|
+
if (config.enableConnectProtocol) {
|
|
455
|
+
this.settingIds.push(H2_SETTINGS_ENABLE_CONNECT_PROTOCOL)
|
|
456
|
+
this.settingValues.push(toI64(1))
|
|
457
|
+
}
|
|
458
|
+
this.restart()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Puts the connection back as the constructor left it, for the next peer:
|
|
463
|
+
* every stream closed, both HPACK tables empty, and the server's SETTINGS
|
|
464
|
+
* in `output` again. It allocates nothing, which is what lets a carrier
|
|
465
|
+
* reuse one connection per slot (WP34 N9).
|
|
466
|
+
*/
|
|
467
|
+
restart(): void {
|
|
468
|
+
const config: Http2Config = this.config
|
|
469
|
+
this.inputStart = 0
|
|
470
|
+
this.inputEnd = 0
|
|
471
|
+
this.outputStart = 0
|
|
472
|
+
this.outputEnd = 0
|
|
473
|
+
this.blockLength = 0
|
|
474
|
+
this.blockStream = 0
|
|
475
|
+
this.blockEndStream = false
|
|
476
|
+
this.blockError = 0
|
|
477
|
+
this.blockFragments = 0
|
|
478
|
+
for (const s of this.streams) {
|
|
479
|
+
s.id = 0
|
|
480
|
+
}
|
|
481
|
+
this.resets.fill(0)
|
|
482
|
+
this.resetAt = 0
|
|
483
|
+
this.closed.fill(0)
|
|
484
|
+
this.closedAt = 0
|
|
485
|
+
this.decoder.table.resize(H2_ZERO)
|
|
486
|
+
this.decoder.table.resize(HPACK_DEFAULT_TABLE_SIZE)
|
|
487
|
+
this.decoder.settingsLimit = HPACK_DEFAULT_TABLE_SIZE
|
|
488
|
+
this.decoder.owedUpdate = -1
|
|
489
|
+
this.decoder.failed = 0
|
|
490
|
+
this.encoder.table.resize(H2_ZERO)
|
|
491
|
+
this.encoder.table.resize(HPACK_DEFAULT_TABLE_SIZE)
|
|
492
|
+
this.encoder.owedMinimum = -1
|
|
493
|
+
this.state = H2_STATE_PREFACE
|
|
494
|
+
this.lastPeerStream = 0
|
|
495
|
+
this.active = 0
|
|
496
|
+
this.peerInitialWindow = toI64(H2_DEFAULT_WINDOW)
|
|
497
|
+
this.peerMaxFrameSize = H2_DEFAULT_MAX_FRAME
|
|
498
|
+
this.peerMaxHeaderList = -1
|
|
499
|
+
this.settingsAcked = false
|
|
500
|
+
this.sendWindow = toI64(H2_DEFAULT_WINDOW)
|
|
501
|
+
this.recvUnacked = 0
|
|
502
|
+
this.sendBlocked = false
|
|
503
|
+
this.goawaySent = false
|
|
504
|
+
this.goawayReceived = false
|
|
505
|
+
this.inputEnded = false
|
|
506
|
+
this.stream = 0
|
|
507
|
+
this.endStream = false
|
|
508
|
+
this.dataStart = 0
|
|
509
|
+
this.dataLength = 0
|
|
510
|
+
this.errorCode = 0
|
|
511
|
+
this.resetByPeer = false
|
|
512
|
+
this.lastStreamId = 0
|
|
513
|
+
this.sentLast = 0
|
|
514
|
+
this.resetCredit = config.resetBudget
|
|
515
|
+
const big: i64 = toI64(H2_DEFAULT_WINDOW)
|
|
516
|
+
this.localInitialWindow = toI64(config.initialWindowSize) > big ? toI64(config.initialWindowSize) : big
|
|
517
|
+
this.recvWindow = toI64(config.connectionWindowSize)
|
|
518
|
+
this.outputEnd = http2WriteSettings(this.output, H2_ZERO, this.settingIds, this.settingValues)
|
|
519
|
+
const raise: i32 = config.connectionWindowSize - H2_DEFAULT_WINDOW
|
|
520
|
+
if (raise > 0) {
|
|
521
|
+
this.outputEnd = http2WriteWindowUpdate(this.output, this.outputEnd, H2_ZERO, raise)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---- Bytes in and out ---------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
/** How many bytes `feed` would take now. */
|
|
528
|
+
inputRoom(): i32 {
|
|
529
|
+
return toI32(this.input.length) - (this.inputEnd - this.inputStart)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Takes as much of `buf[off, off + len)` as the input buffer has room for,
|
|
534
|
+
* and answers how much; the rest is the caller's to feed again once `next`
|
|
535
|
+
* has read what is there. Moves the unread bytes to the front of the
|
|
536
|
+
* buffer, which ends the last H2_DATA's window. Once the connection has
|
|
537
|
+
* failed it takes everything and keeps nothing.
|
|
538
|
+
*/
|
|
539
|
+
feed(buf: u8[], off: i32, len: i32): i32 {
|
|
540
|
+
http2CheckWindow("Http2Connection.feed", buf, off, len)
|
|
541
|
+
if (this.state === H2_STATE_FAILED) {
|
|
542
|
+
return len
|
|
543
|
+
}
|
|
544
|
+
const room: i32 = this.inputTail()
|
|
545
|
+
const n: i32 = len < room ? len : room
|
|
546
|
+
for (let k: i32 = 0; k < n; k++) {
|
|
547
|
+
this.input[this.inputEnd + k] = buf[off + k]
|
|
548
|
+
}
|
|
549
|
+
this.inputEnd = this.inputEnd + n
|
|
550
|
+
return n
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Moves the unread input to the front of its buffer, which ends the last
|
|
555
|
+
* H2_DATA's window, and answers the room after it: where a carrier may read
|
|
556
|
+
* straight into `input` from `inputEnd`, then `received` what it read.
|
|
557
|
+
*/
|
|
558
|
+
inputTail(): i32 {
|
|
559
|
+
this.inputEnd = http2Compact(this.input, this.inputStart, this.inputEnd)
|
|
560
|
+
this.inputStart = 0
|
|
561
|
+
return toI32(this.input.length) - this.inputEnd
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Takes `n` bytes a carrier read into `input` at `inputEnd`, after `inputTail` gave it the room. */
|
|
565
|
+
received(n: i32): void {
|
|
566
|
+
const room: i32 = toI32(this.input.length) - this.inputEnd
|
|
567
|
+
if (n < 0 || n > room) {
|
|
568
|
+
panic(`Http2Connection.received: ${n} bytes into ${room} of room`)
|
|
569
|
+
}
|
|
570
|
+
if (this.state !== H2_STATE_FAILED) {
|
|
571
|
+
this.inputEnd = this.inputEnd + n
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Marks the first `n` bytes of the output sent. */
|
|
576
|
+
consume(n: i32): void {
|
|
577
|
+
const pending: i32 = this.outputEnd - this.outputStart
|
|
578
|
+
this.outputStart = this.outputStart + (n < pending ? n : pending)
|
|
579
|
+
if (this.outputStart === this.outputEnd) {
|
|
580
|
+
this.outputStart = 0
|
|
581
|
+
this.outputEnd = 0
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Whether the output holds bytes to send. */
|
|
586
|
+
wantsWrite(): boolean {
|
|
587
|
+
return this.outputEnd > this.outputStart
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Whether the connection is over: everything is sent, and it failed, or no
|
|
592
|
+
* stream is left after a GOAWAY went either way or after the peer's byte
|
|
593
|
+
* stream ended (`endInput`) with nothing of it left unread.
|
|
594
|
+
*/
|
|
595
|
+
isDone(): boolean {
|
|
596
|
+
if (this.wantsWrite()) {
|
|
597
|
+
return false
|
|
598
|
+
}
|
|
599
|
+
const unread: boolean = this.inputEnd > this.inputStart
|
|
600
|
+
const over: boolean = this.goawaySent || this.goawayReceived || (this.inputEnded && !unread)
|
|
601
|
+
return this.state === H2_STATE_FAILED || (over && this.active === 0)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* The peer's byte stream has ended, so nothing more will be fed: once the
|
|
606
|
+
* requests it already sent are answered, the connection is done.
|
|
607
|
+
*/
|
|
608
|
+
endInput(): void {
|
|
609
|
+
this.inputEnded = true
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** The room left at the end of the output, once what was sent is moved out of the way. */
|
|
613
|
+
room(): i32 {
|
|
614
|
+
this.outputEnd = http2Compact(this.output, this.outputStart, this.outputEnd)
|
|
615
|
+
this.outputStart = 0
|
|
616
|
+
return toI32(this.output.length) - this.outputEnd
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** The room DATA and HEADERS may use: all but the control reserve. */
|
|
620
|
+
bodyRoom(): i32 {
|
|
621
|
+
const r: i32 = this.room() - H2_CONTROL_RESERVE
|
|
622
|
+
return r > 0 ? r : 0
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Appends a frame a writer put at the end of the output, when it fit (`end >= 0`), and answers whether it did. */
|
|
626
|
+
wrote(end: i32): boolean {
|
|
627
|
+
if (end >= 0) {
|
|
628
|
+
this.outputEnd = end
|
|
629
|
+
}
|
|
630
|
+
return end >= 0
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ---- Streams ------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
/** The slot holding stream `id`, or `null`. */
|
|
636
|
+
find(id: i32): Http2Stream | null {
|
|
637
|
+
if (id <= 0) {
|
|
638
|
+
return null
|
|
639
|
+
}
|
|
640
|
+
for (const s of this.streams) {
|
|
641
|
+
if (s.id === id) {
|
|
642
|
+
return s
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return null
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Opens stream `id` in a free slot; the caller has checked there is one. */
|
|
649
|
+
open(id: i32): Http2Stream {
|
|
650
|
+
for (const s of this.streams) {
|
|
651
|
+
if (s.id === 0) {
|
|
652
|
+
s.id = id
|
|
653
|
+
s.sendWindow = this.peerInitialWindow
|
|
654
|
+
s.recvWindow = this.localInitialWindow
|
|
655
|
+
s.recvUnacked = 0
|
|
656
|
+
s.contentLength = -1
|
|
657
|
+
s.received = 0
|
|
658
|
+
s.remoteEnded = false
|
|
659
|
+
s.localEnded = false
|
|
660
|
+
s.responded = false
|
|
661
|
+
s.blocked = false
|
|
662
|
+
this.active = this.active + 1
|
|
663
|
+
return s
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
panic("Http2Connection.open: no free slot")
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Frees the slot of a stream that is closed. */
|
|
670
|
+
release(s: Http2Stream): void {
|
|
671
|
+
if (s.id !== 0) {
|
|
672
|
+
this.closedAt = http2Remember(this.closed, this.closedAt, s.id)
|
|
673
|
+
s.id = 0
|
|
674
|
+
this.active = this.active - 1
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** The peer ended stream `s`; closes it when the server had too. */
|
|
679
|
+
remoteEnd(s: Http2Stream): void {
|
|
680
|
+
s.remoteEnded = true
|
|
681
|
+
if (s.localEnded) {
|
|
682
|
+
this.release(s)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** The server ended stream `s`; closes it when the peer had too, and credits the reset budget. */
|
|
687
|
+
localEnd(s: Http2Stream): void {
|
|
688
|
+
s.localEnded = true
|
|
689
|
+
if (this.resetCredit < this.config.resetBudget) {
|
|
690
|
+
this.resetCredit = this.resetCredit + 1
|
|
691
|
+
}
|
|
692
|
+
if (s.remoteEnded) {
|
|
693
|
+
this.release(s)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Whether the server reset stream `id` recently, so frames still in flight for it are ignored (§5.4.2). */
|
|
698
|
+
recentlyReset(id: i32): boolean {
|
|
699
|
+
return id > 0 && this.resets.indexOf(id) >= 0
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Whether stream `id` is one the connection remembers closing. A HEADERS
|
|
704
|
+
* below the highest identifier on a stream it does not remember was never
|
|
705
|
+
* opened, and is the out-of-order identifier §5.1.1 calls PROTOCOL_ERROR
|
|
706
|
+
* rather than a frame on a closed stream (STREAM_CLOSED, §5.1).
|
|
707
|
+
*/
|
|
708
|
+
recentlyClosed(id: i32): boolean {
|
|
709
|
+
return id > 0 && this.closed.indexOf(id) >= 0
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Sends RST_STREAM for `id` with `code`, frees its slot if it has one, and
|
|
714
|
+
* remembers it; answers false, changing nothing, when the frame did not fit.
|
|
715
|
+
*/
|
|
716
|
+
resetStream(id: i32, code: i32): boolean {
|
|
717
|
+
if (!this.wrote(http2WriteRstStream(this.output, this.outputEnd, id, toI64(code)))) {
|
|
718
|
+
return false
|
|
719
|
+
}
|
|
720
|
+
const s: Http2Stream | null = this.find(id)
|
|
721
|
+
if (s !== null) {
|
|
722
|
+
this.release(s)
|
|
723
|
+
}
|
|
724
|
+
this.resetAt = http2Remember(this.resets, this.resetAt, id)
|
|
725
|
+
return true
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** A stream error on `s`: resets it, and answers the H2_RESET that tells the program. */
|
|
729
|
+
streamError(s: Http2Stream, code: i32): i32 {
|
|
730
|
+
const id: i32 = s.id
|
|
731
|
+
this.resetStream(id, code)
|
|
732
|
+
this.stream = id
|
|
733
|
+
this.errorCode = toI64(code)
|
|
734
|
+
this.resetByPeer = false
|
|
735
|
+
return H2_RESET
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Writes GOAWAY with `code`, naming the last stream the peer had opened when
|
|
740
|
+
* the server's first GOAWAY went: no later one may name a higher stream
|
|
741
|
+
* (§6.8). Answers whether it fit.
|
|
742
|
+
*/
|
|
743
|
+
sendGoaway(code: i32): boolean {
|
|
744
|
+
const last: i32 = this.goawaySent ? this.sentLast : this.lastPeerStream
|
|
745
|
+
if (
|
|
746
|
+
!this.wrote(
|
|
747
|
+
http2WriteGoaway(this.output, this.outputEnd, last, toI64(code), this.noDebug, H2_ZERO, H2_ZERO)
|
|
748
|
+
)
|
|
749
|
+
) {
|
|
750
|
+
return false
|
|
751
|
+
}
|
|
752
|
+
this.goawaySent = true
|
|
753
|
+
this.sentLast = last
|
|
754
|
+
return true
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** A connection error: queues GOAWAY with `code` and answers H2_ERROR. */
|
|
758
|
+
fail(code: i32): i32 {
|
|
759
|
+
if (this.state !== H2_STATE_FAILED) {
|
|
760
|
+
this.state = H2_STATE_FAILED
|
|
761
|
+
this.errorCode = toI64(code)
|
|
762
|
+
this.sendGoaway(code)
|
|
763
|
+
this.inputStart = 0
|
|
764
|
+
this.inputEnd = 0
|
|
765
|
+
this.blockStream = 0
|
|
766
|
+
}
|
|
767
|
+
return H2_ERROR
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Hands `n` received bytes back to the connection's window, and to `s`'s
|
|
772
|
+
* when it is still open for receiving, with a WINDOW_UPDATE once half a
|
|
773
|
+
* window is owed.
|
|
774
|
+
*/
|
|
775
|
+
credit(s: Http2Stream | null, n: i32): void {
|
|
776
|
+
this.recvUnacked = this.recvUnacked + toI64(n)
|
|
777
|
+
const half: i64 = toI64(this.config.connectionWindowSize / 2)
|
|
778
|
+
if (this.recvUnacked > toI64(0) && this.recvUnacked >= half) {
|
|
779
|
+
const inc: i32 = toI32(this.recvUnacked)
|
|
780
|
+
this.wrote(http2WriteWindowUpdate(this.output, this.outputEnd, H2_ZERO, inc))
|
|
781
|
+
this.recvWindow = this.recvWindow + this.recvUnacked
|
|
782
|
+
this.recvUnacked = 0
|
|
783
|
+
}
|
|
784
|
+
if (s !== null && !s.remoteEnded) {
|
|
785
|
+
s.recvUnacked = s.recvUnacked + n
|
|
786
|
+
if (s.recvUnacked > 0 && s.recvUnacked >= this.config.initialWindowSize / 2) {
|
|
787
|
+
this.wrote(http2WriteWindowUpdate(this.output, this.outputEnd, s.id, s.recvUnacked))
|
|
788
|
+
s.recvWindow = s.recvWindow + toI64(s.recvUnacked)
|
|
789
|
+
s.recvUnacked = 0
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// ---- Reading ------------------------------------------------------------------
|
|
795
|
+
|
|
796
|
+
/** Reads frames until one is an event for the program, and answers it. */
|
|
797
|
+
next(): i32 {
|
|
798
|
+
while (true) {
|
|
799
|
+
if (this.state === H2_STATE_FAILED) {
|
|
800
|
+
return H2_ERROR
|
|
801
|
+
}
|
|
802
|
+
if (this.room() < H2_CONTROL_RESERVE) {
|
|
803
|
+
return H2_NEED_MORE
|
|
804
|
+
}
|
|
805
|
+
const available: i32 = this.inputEnd - this.inputStart
|
|
806
|
+
if (this.state === H2_STATE_PREFACE) {
|
|
807
|
+
if (available < H2_PREFACE_SIZE) {
|
|
808
|
+
return H2_NEED_MORE
|
|
809
|
+
}
|
|
810
|
+
if (!http2IsPreface(this.input, this.inputStart)) {
|
|
811
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
812
|
+
}
|
|
813
|
+
this.inputStart = this.inputStart + H2_PREFACE_SIZE
|
|
814
|
+
this.state = H2_STATE_SETTINGS
|
|
815
|
+
continue
|
|
816
|
+
}
|
|
817
|
+
const f: Http2Frame = this.frame
|
|
818
|
+
if (!http2ReadHeader(f, this.input, this.inputStart, available)) {
|
|
819
|
+
return H2_NEED_MORE
|
|
820
|
+
}
|
|
821
|
+
if (f.length > this.config.maxFrameSize) {
|
|
822
|
+
return this.fail(H2_FRAME_SIZE_ERROR)
|
|
823
|
+
}
|
|
824
|
+
if (available < H2_FRAME_HEADER_SIZE + f.length) {
|
|
825
|
+
return H2_NEED_MORE
|
|
826
|
+
}
|
|
827
|
+
const error: i32 = http2ParseFrame(f, this.input, this.inputStart, this.config.maxFrameSize)
|
|
828
|
+
this.inputStart = this.inputStart + H2_FRAME_HEADER_SIZE + f.length
|
|
829
|
+
if (error !== H2_NO_ERROR) {
|
|
830
|
+
return this.fail(error)
|
|
831
|
+
}
|
|
832
|
+
const event: i32 = this.handleFrame(f)
|
|
833
|
+
if (event !== H2_NEED_MORE) {
|
|
834
|
+
return event
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** One frame that parsed: the rules no single frame type owns, then the type's own. */
|
|
840
|
+
handleFrame(f: Http2Frame): i32 {
|
|
841
|
+
if (this.blockStream !== 0 && (f.type !== H2_FRAME_CONTINUATION || f.streamId !== this.blockStream)) {
|
|
842
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
843
|
+
}
|
|
844
|
+
if (this.state === H2_STATE_SETTINGS) {
|
|
845
|
+
if (f.type !== H2_FRAME_SETTINGS || f.has(H2_FLAG_ACK)) {
|
|
846
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
847
|
+
}
|
|
848
|
+
this.state = H2_STATE_OPEN
|
|
849
|
+
}
|
|
850
|
+
switch (f.type) {
|
|
851
|
+
case H2_FRAME_DATA:
|
|
852
|
+
return this.handleData(f)
|
|
853
|
+
case H2_FRAME_HEADERS:
|
|
854
|
+
return this.handleHeaders(f)
|
|
855
|
+
case H2_FRAME_PRIORITY:
|
|
856
|
+
return this.handlePriority(f)
|
|
857
|
+
case H2_FRAME_RST_STREAM:
|
|
858
|
+
return this.handleRstStream(f)
|
|
859
|
+
case H2_FRAME_SETTINGS:
|
|
860
|
+
return this.handleSettings(f)
|
|
861
|
+
case H2_FRAME_PUSH_PROMISE:
|
|
862
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
863
|
+
case H2_FRAME_PING:
|
|
864
|
+
if (!f.has(H2_FLAG_ACK)) {
|
|
865
|
+
this.wrote(http2WritePing(this.output, this.outputEnd, this.input, f.contentStart, true))
|
|
866
|
+
}
|
|
867
|
+
return H2_NEED_MORE
|
|
868
|
+
case H2_FRAME_GOAWAY:
|
|
869
|
+
this.goawayReceived = true
|
|
870
|
+
this.lastStreamId = f.lastStreamId
|
|
871
|
+
this.errorCode = f.errorCode
|
|
872
|
+
return H2_GOAWAY
|
|
873
|
+
case H2_FRAME_WINDOW_UPDATE:
|
|
874
|
+
return this.handleWindowUpdate(f)
|
|
875
|
+
case H2_FRAME_CONTINUATION:
|
|
876
|
+
return this.handleContinuation(f)
|
|
877
|
+
default:
|
|
878
|
+
return H2_NEED_MORE
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Whether `id` names an idle stream: one the peer has not opened yet. */
|
|
883
|
+
isIdle(id: i32): boolean {
|
|
884
|
+
// An even identifier is the server's to open (§5.1.1), and this server
|
|
885
|
+
// opens none, so every even stream is idle for as long as it lives.
|
|
886
|
+
return id % 2 === 0 || id > this.lastPeerStream
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Whether frames on stream `id`, which has no slot, are dropped without a
|
|
891
|
+
* word: the server reset it and these were in flight (§5.4.2), or it was
|
|
892
|
+
* opened after the server's GOAWAY and so never opened at all (§6.8).
|
|
893
|
+
*/
|
|
894
|
+
discarded(id: i32): boolean {
|
|
895
|
+
return this.recentlyReset(id) || (this.goawaySent && id > this.sentLast)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** DATA (§6.1): flow control first, on the whole payload, then the stream's state. */
|
|
899
|
+
handleData(f: Http2Frame): i32 {
|
|
900
|
+
const id: i32 = f.streamId
|
|
901
|
+
const size: i32 = f.length
|
|
902
|
+
const s: Http2Stream | null = this.find(id)
|
|
903
|
+
if (s === null && this.isIdle(id)) {
|
|
904
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
905
|
+
}
|
|
906
|
+
if (toI64(size) > this.recvWindow) {
|
|
907
|
+
return this.fail(H2_FLOW_CONTROL_ERROR)
|
|
908
|
+
}
|
|
909
|
+
this.recvWindow = this.recvWindow - toI64(size)
|
|
910
|
+
if (s === null) {
|
|
911
|
+
this.credit(null, size)
|
|
912
|
+
if (!this.discarded(id)) {
|
|
913
|
+
this.resetStream(id, H2_STREAM_CLOSED)
|
|
914
|
+
}
|
|
915
|
+
return H2_NEED_MORE
|
|
916
|
+
}
|
|
917
|
+
const end: boolean = f.has(H2_FLAG_END_STREAM)
|
|
918
|
+
const received: i64 = s.received + toI64(f.contentLength)
|
|
919
|
+
const declared: boolean = s.contentLength >= toI64(0)
|
|
920
|
+
let error: i32 = H2_NO_ERROR
|
|
921
|
+
if (s.remoteEnded) {
|
|
922
|
+
error = H2_STREAM_CLOSED
|
|
923
|
+
} else if (toI64(size) > s.recvWindow) {
|
|
924
|
+
error = H2_FLOW_CONTROL_ERROR
|
|
925
|
+
} else if (declared && (received > s.contentLength || (end && received !== s.contentLength))) {
|
|
926
|
+
error = H2_PROTOCOL_ERROR
|
|
927
|
+
}
|
|
928
|
+
if (error !== H2_NO_ERROR) {
|
|
929
|
+
this.credit(null, size)
|
|
930
|
+
return this.streamError(s, error)
|
|
931
|
+
}
|
|
932
|
+
s.recvWindow = s.recvWindow - toI64(size)
|
|
933
|
+
s.received = received
|
|
934
|
+
this.credit(end ? null : s, size)
|
|
935
|
+
this.stream = id
|
|
936
|
+
this.endStream = end
|
|
937
|
+
this.data = this.input
|
|
938
|
+
this.dataStart = f.contentStart
|
|
939
|
+
this.dataLength = f.contentLength
|
|
940
|
+
if (end) {
|
|
941
|
+
this.remoteEnd(s)
|
|
942
|
+
} else if (f.contentLength === 0) {
|
|
943
|
+
return H2_NEED_MORE
|
|
944
|
+
}
|
|
945
|
+
return H2_DATA
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** Appends a fragment of a header block; a block past `maxHeaderBlock` or `H2_MAX_FRAGMENTS` ends the connection. */
|
|
949
|
+
gather(f: Http2Frame): boolean {
|
|
950
|
+
this.blockFragments = this.blockFragments + 1
|
|
951
|
+
if (
|
|
952
|
+
this.blockFragments > H2_MAX_FRAGMENTS ||
|
|
953
|
+
f.contentLength > toI32(this.block.length) - this.blockLength
|
|
954
|
+
) {
|
|
955
|
+
return false
|
|
956
|
+
}
|
|
957
|
+
for (let k: i32 = 0; k < f.contentLength; k++) {
|
|
958
|
+
this.block[this.blockLength + k] = this.input[f.contentStart + k]
|
|
959
|
+
}
|
|
960
|
+
this.blockLength = this.blockLength + f.contentLength
|
|
961
|
+
return true
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** HEADERS (§6.2): starts a header block, and ends it when END_HEADERS says so. */
|
|
965
|
+
handleHeaders(f: Http2Frame): i32 {
|
|
966
|
+
this.blockStream = f.streamId
|
|
967
|
+
this.blockEndStream = f.has(H2_FLAG_END_STREAM)
|
|
968
|
+
this.blockError = f.streamError
|
|
969
|
+
this.blockLength = 0
|
|
970
|
+
this.blockFragments = 0
|
|
971
|
+
return this.handleContinuation(f)
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** CONTINUATION (§6.10): only inside a header block, which `handleFrame` has checked it continues. */
|
|
975
|
+
handleContinuation(f: Http2Frame): i32 {
|
|
976
|
+
if (this.blockStream === 0) {
|
|
977
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
978
|
+
}
|
|
979
|
+
if (!this.gather(f)) {
|
|
980
|
+
return this.fail(H2_ENHANCE_YOUR_CALM)
|
|
981
|
+
}
|
|
982
|
+
return f.has(H2_FLAG_END_HEADERS) ? this.endBlock() : H2_NEED_MORE
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* A whole header block: decoded first, whatever becomes of its stream,
|
|
987
|
+
* because the decoder's table must stay in step with the peer's encoder
|
|
988
|
+
* (§4.3); then a new stream's request, or an open stream's trailers.
|
|
989
|
+
*/
|
|
990
|
+
endBlock(): i32 {
|
|
991
|
+
const id: i32 = this.blockStream
|
|
992
|
+
this.blockStream = 0
|
|
993
|
+
const decoded: i32 = this.decoder.decode(this.block, H2_ZERO, this.blockLength)
|
|
994
|
+
if (decoded < 0) {
|
|
995
|
+
return this.fail(H2_COMPRESSION_ERROR)
|
|
996
|
+
}
|
|
997
|
+
const s: Http2Stream | null = this.find(id)
|
|
998
|
+
if (s !== null) {
|
|
999
|
+
return this.trailers(s, decoded)
|
|
1000
|
+
}
|
|
1001
|
+
if (!this.isIdle(id)) {
|
|
1002
|
+
if (this.discarded(id)) {
|
|
1003
|
+
return H2_NEED_MORE
|
|
1004
|
+
}
|
|
1005
|
+
return this.fail(this.recentlyClosed(id) ? H2_STREAM_CLOSED : H2_PROTOCOL_ERROR)
|
|
1006
|
+
}
|
|
1007
|
+
if (id % 2 === 0) {
|
|
1008
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
1009
|
+
}
|
|
1010
|
+
this.lastPeerStream = id
|
|
1011
|
+
if (this.goawaySent) {
|
|
1012
|
+
return H2_NEED_MORE
|
|
1013
|
+
}
|
|
1014
|
+
if (this.blockError !== 0) {
|
|
1015
|
+
this.resetStream(id, this.blockError)
|
|
1016
|
+
return H2_NEED_MORE
|
|
1017
|
+
}
|
|
1018
|
+
if (decoded === HPACK_LIST_TOO_LARGE) {
|
|
1019
|
+
this.tooLarge(id)
|
|
1020
|
+
return H2_NEED_MORE
|
|
1021
|
+
}
|
|
1022
|
+
if (this.active >= this.config.maxStreams) {
|
|
1023
|
+
this.resetStream(id, H2_REFUSED_STREAM)
|
|
1024
|
+
return H2_NEED_MORE
|
|
1025
|
+
}
|
|
1026
|
+
const shape: i32 = this.fields.readRequest(
|
|
1027
|
+
this.decoder.names,
|
|
1028
|
+
this.decoder.values,
|
|
1029
|
+
this.config.enableConnectProtocol
|
|
1030
|
+
)
|
|
1031
|
+
if (shape !== HTTP_FIELDS_OK || (this.blockEndStream && this.fields.contentLength > toI64(0))) {
|
|
1032
|
+
this.resetStream(id, H2_PROTOCOL_ERROR)
|
|
1033
|
+
return H2_NEED_MORE
|
|
1034
|
+
}
|
|
1035
|
+
const opened: Http2Stream = this.open(id)
|
|
1036
|
+
opened.contentLength = this.fields.contentLength
|
|
1037
|
+
this.stream = id
|
|
1038
|
+
this.endStream = this.blockEndStream
|
|
1039
|
+
if (this.blockEndStream) {
|
|
1040
|
+
this.remoteEnd(opened)
|
|
1041
|
+
}
|
|
1042
|
+
return H2_REQUEST
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** A header block on an open stream: trailers, which must end it (§8.1). */
|
|
1046
|
+
trailers(s: Http2Stream, decoded: i32): i32 {
|
|
1047
|
+
if (s.remoteEnded) {
|
|
1048
|
+
return this.streamError(s, H2_STREAM_CLOSED)
|
|
1049
|
+
}
|
|
1050
|
+
if (this.blockError !== 0) {
|
|
1051
|
+
return this.streamError(s, this.blockError)
|
|
1052
|
+
}
|
|
1053
|
+
if (
|
|
1054
|
+
!this.blockEndStream ||
|
|
1055
|
+
decoded === HPACK_LIST_TOO_LARGE ||
|
|
1056
|
+
this.fields.readTrailers(this.decoder.names, this.decoder.values) !== HTTP_FIELDS_OK ||
|
|
1057
|
+
(s.contentLength >= toI64(0) && s.received !== s.contentLength)
|
|
1058
|
+
) {
|
|
1059
|
+
return this.streamError(s, H2_PROTOCOL_ERROR)
|
|
1060
|
+
}
|
|
1061
|
+
this.stream = s.id
|
|
1062
|
+
this.endStream = true
|
|
1063
|
+
this.remoteEnd(s)
|
|
1064
|
+
return H2_TRAILERS
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* A request whose header list passed `maxHeaderListSize`: a 431 with
|
|
1069
|
+
* END_STREAM, then a RST_STREAM with NO_ERROR when the peer is still
|
|
1070
|
+
* sending (§8.1), so the stream never reaches the program.
|
|
1071
|
+
*/
|
|
1072
|
+
tooLarge(id: i32): void {
|
|
1073
|
+
this.encodeStatus(H2_STATUS_TOO_LARGE)
|
|
1074
|
+
const n: i32 = toI32(this.encoded.length)
|
|
1075
|
+
this.wrote(
|
|
1076
|
+
http2WriteHeaders(
|
|
1077
|
+
this.output,
|
|
1078
|
+
this.outputEnd,
|
|
1079
|
+
id,
|
|
1080
|
+
this.encoded,
|
|
1081
|
+
H2_ZERO,
|
|
1082
|
+
n,
|
|
1083
|
+
H2_FLAG_END_STREAM | H2_FLAG_END_HEADERS,
|
|
1084
|
+
H2_NO_PAD
|
|
1085
|
+
)
|
|
1086
|
+
)
|
|
1087
|
+
if (!this.blockEndStream) {
|
|
1088
|
+
this.resetStream(id, H2_NO_ERROR)
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/** Empties `encoded` and starts a block with `:status`, or with nothing for 0 (trailers). */
|
|
1093
|
+
encodeStatus(status: i32): void {
|
|
1094
|
+
while (toI32(this.encoded.length) > 0) {
|
|
1095
|
+
this.encoded.pop()
|
|
1096
|
+
}
|
|
1097
|
+
if (status > 0) {
|
|
1098
|
+
this.statusValue[0] = toU8(status / 100 + H2_DIGIT_ZERO)
|
|
1099
|
+
this.statusValue[1] = toU8(((status / 10) % 10) + H2_DIGIT_ZERO)
|
|
1100
|
+
this.statusValue[2] = toU8((status % 10) + H2_DIGIT_ZERO)
|
|
1101
|
+
this.encoder.encodeField(this.encoded, this.statusName, this.statusValue, HPACK_INDEX_WITHOUT, false)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** PRIORITY (§6.3): checked and otherwise ignored, and it opens nothing. */
|
|
1106
|
+
handlePriority(f: Http2Frame): i32 {
|
|
1107
|
+
if (f.streamError === 0) {
|
|
1108
|
+
return H2_NEED_MORE
|
|
1109
|
+
}
|
|
1110
|
+
const s: Http2Stream | null = this.find(f.streamId)
|
|
1111
|
+
if (s !== null) {
|
|
1112
|
+
return this.streamError(s, f.streamError)
|
|
1113
|
+
}
|
|
1114
|
+
this.resetStream(f.streamId, f.streamError)
|
|
1115
|
+
return H2_NEED_MORE
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** RST_STREAM (§6.4): the stream is closed; on an idle one it is a connection error. */
|
|
1119
|
+
handleRstStream(f: Http2Frame): i32 {
|
|
1120
|
+
if (this.isIdle(f.streamId)) {
|
|
1121
|
+
return this.fail(H2_PROTOCOL_ERROR)
|
|
1122
|
+
}
|
|
1123
|
+
const s: Http2Stream | null = this.find(f.streamId)
|
|
1124
|
+
if (s === null) {
|
|
1125
|
+
return H2_NEED_MORE
|
|
1126
|
+
}
|
|
1127
|
+
this.release(s)
|
|
1128
|
+
this.resetCredit = this.resetCredit - 1
|
|
1129
|
+
if (this.resetCredit < 0) {
|
|
1130
|
+
return this.fail(H2_ENHANCE_YOUR_CALM)
|
|
1131
|
+
}
|
|
1132
|
+
this.stream = f.streamId
|
|
1133
|
+
this.errorCode = f.errorCode
|
|
1134
|
+
this.resetByPeer = true
|
|
1135
|
+
return H2_RESET
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* SETTINGS (§6.5): the peer's acknowledgement puts the server's own
|
|
1140
|
+
* settings in force; anything else is the peer's, each value checked, then
|
|
1141
|
+
* acknowledged. A new SETTINGS_INITIAL_WINDOW_SIZE moves every stream's send
|
|
1142
|
+
* window by the difference (§6.9.2), and opening one answers H2_WINDOW.
|
|
1143
|
+
*/
|
|
1144
|
+
handleSettings(f: Http2Frame): i32 {
|
|
1145
|
+
if (f.has(H2_FLAG_ACK)) {
|
|
1146
|
+
if (!this.settingsAcked) {
|
|
1147
|
+
this.settingsAcked = true
|
|
1148
|
+
this.decoder.setSettingsLimit(this.config.headerTableSize)
|
|
1149
|
+
const delta: i64 = toI64(this.config.initialWindowSize) - this.localInitialWindow
|
|
1150
|
+
this.localInitialWindow = toI64(this.config.initialWindowSize)
|
|
1151
|
+
for (const s of this.streams) {
|
|
1152
|
+
if (s.id !== 0) {
|
|
1153
|
+
s.recvWindow = s.recvWindow + delta
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return H2_NEED_MORE
|
|
1158
|
+
}
|
|
1159
|
+
let opened: boolean = false
|
|
1160
|
+
const n: i32 = http2SettingCount(f)
|
|
1161
|
+
for (let k: i32 = 0; k < n; k++) {
|
|
1162
|
+
const id: i32 = http2SettingId(f, this.input, k)
|
|
1163
|
+
const value: i64 = http2SettingValue(f, this.input, k)
|
|
1164
|
+
const error: i32 = http2SettingError(id, value)
|
|
1165
|
+
if (error !== H2_NO_ERROR) {
|
|
1166
|
+
return this.fail(error)
|
|
1167
|
+
}
|
|
1168
|
+
if (id === H2_SETTINGS_HEADER_TABLE_SIZE) {
|
|
1169
|
+
const size: i32 = toI32(Math.min(value, toI64(HPACK_DEFAULT_TABLE_SIZE)))
|
|
1170
|
+
if (size !== this.encoder.table.maxSize) {
|
|
1171
|
+
this.encoder.setMaxTableSize(size)
|
|
1172
|
+
}
|
|
1173
|
+
} else if (id === H2_SETTINGS_INITIAL_WINDOW_SIZE) {
|
|
1174
|
+
const delta: i64 = value - this.peerInitialWindow
|
|
1175
|
+
this.peerInitialWindow = value
|
|
1176
|
+
for (const s of this.streams) {
|
|
1177
|
+
if (s.id !== 0) {
|
|
1178
|
+
s.sendWindow = s.sendWindow + delta
|
|
1179
|
+
if (s.sendWindow > toI64(H2_MAX_WINDOW)) {
|
|
1180
|
+
return this.fail(H2_FLOW_CONTROL_ERROR)
|
|
1181
|
+
}
|
|
1182
|
+
if (delta > toI64(0) && s.blocked) {
|
|
1183
|
+
s.blocked = false
|
|
1184
|
+
opened = true
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} else if (id === H2_SETTINGS_MAX_FRAME_SIZE) {
|
|
1189
|
+
this.peerMaxFrameSize = toI32(value)
|
|
1190
|
+
} else if (id === H2_SETTINGS_MAX_HEADER_LIST_SIZE) {
|
|
1191
|
+
this.peerMaxHeaderList = value
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
this.wrote(http2WriteSettingsAck(this.output, this.outputEnd))
|
|
1195
|
+
if (opened) {
|
|
1196
|
+
this.stream = 0
|
|
1197
|
+
return H2_WINDOW
|
|
1198
|
+
}
|
|
1199
|
+
return H2_NEED_MORE
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
/** WINDOW_UPDATE (§6.9): a send window grows, and a write it held back may go on. */
|
|
1203
|
+
handleWindowUpdate(f: Http2Frame): i32 {
|
|
1204
|
+
const inc: i64 = toI64(f.increment)
|
|
1205
|
+
if (f.streamId === 0) {
|
|
1206
|
+
this.sendWindow = this.sendWindow + inc
|
|
1207
|
+
if (this.sendWindow > toI64(H2_MAX_WINDOW)) {
|
|
1208
|
+
return this.fail(H2_FLOW_CONTROL_ERROR)
|
|
1209
|
+
}
|
|
1210
|
+
if (this.sendBlocked) {
|
|
1211
|
+
this.sendBlocked = false
|
|
1212
|
+
this.stream = 0
|
|
1213
|
+
return H2_WINDOW
|
|
1214
|
+
}
|
|
1215
|
+
return H2_NEED_MORE
|
|
1216
|
+
}
|
|
1217
|
+
const s: Http2Stream | null = this.find(f.streamId)
|
|
1218
|
+
if (s === null) {
|
|
1219
|
+
return this.isIdle(f.streamId) ? this.fail(H2_PROTOCOL_ERROR) : H2_NEED_MORE
|
|
1220
|
+
}
|
|
1221
|
+
if (f.streamError !== 0) {
|
|
1222
|
+
return this.streamError(s, f.streamError)
|
|
1223
|
+
}
|
|
1224
|
+
s.sendWindow = s.sendWindow + inc
|
|
1225
|
+
if (s.sendWindow > toI64(H2_MAX_WINDOW)) {
|
|
1226
|
+
return this.streamError(s, H2_FLOW_CONTROL_ERROR)
|
|
1227
|
+
}
|
|
1228
|
+
if (s.blocked) {
|
|
1229
|
+
s.blocked = false
|
|
1230
|
+
this.stream = s.id
|
|
1231
|
+
return H2_WINDOW
|
|
1232
|
+
}
|
|
1233
|
+
return H2_NEED_MORE
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// ---- Writing ------------------------------------------------------------------
|
|
1237
|
+
|
|
1238
|
+
/** The stream the program may still send on, or `null`. */
|
|
1239
|
+
writable(id: i32): Http2Stream | null {
|
|
1240
|
+
if (this.state === H2_STATE_FAILED) {
|
|
1241
|
+
return null
|
|
1242
|
+
}
|
|
1243
|
+
const s: Http2Stream | null = this.find(id)
|
|
1244
|
+
return s !== null && !s.localEnded ? s : null
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Encodes `:status` and `names`/`values` and sends the block as HEADERS and
|
|
1249
|
+
* as many CONTINUATION frames as the peer's SETTINGS_MAX_FRAME_SIZE needs,
|
|
1250
|
+
* with END_STREAM when `endStream`. Answers 0, `H2_INVALID` for fields
|
|
1251
|
+
* `httpFieldsCheckOutgoing` refuses or a status outside 100 to 599,
|
|
1252
|
+
* `H2_AGAIN` when the output has no room for it yet, or `H2_CLOSED`.
|
|
1253
|
+
* Nothing is encoded until it is sure to be sent, so the HPACK state never
|
|
1254
|
+
* moves for a block that was not.
|
|
1255
|
+
*/
|
|
1256
|
+
sendHead(s: Http2Stream, status: i32, names: u8[][], values: u8[][], endStream: boolean): i32 {
|
|
1257
|
+
if (httpFieldsCheckOutgoing(names, values) !== HTTP_FIELDS_OK) {
|
|
1258
|
+
return H2_INVALID
|
|
1259
|
+
}
|
|
1260
|
+
// A literal costs its strings and at most eleven bytes of prefixes; two
|
|
1261
|
+
// table size updates and `:status` are inside the forty-eight on top.
|
|
1262
|
+
let bound: i64 = toI64(48)
|
|
1263
|
+
for (let k: i32 = 0; k < toI32(names.length) && k < toI32(values.length); k++) {
|
|
1264
|
+
bound = bound + toI64(names[k].length) + toI64(values[k].length) + toI64(11)
|
|
1265
|
+
}
|
|
1266
|
+
const frames: i64 = bound / toI64(this.peerMaxFrameSize) + toI64(1)
|
|
1267
|
+
const need: i64 = bound + frames * toI64(H2_FRAME_HEADER_SIZE)
|
|
1268
|
+
if (need > toI64(this.bodyRoom())) {
|
|
1269
|
+
return toI64(this.output.length) < need + toI64(H2_CONTROL_RESERVE) ? H2_INVALID : H2_AGAIN
|
|
1270
|
+
}
|
|
1271
|
+
this.encodeStatus(status)
|
|
1272
|
+
for (let k: i32 = 0; k < toI32(names.length) && k < toI32(values.length); k++) {
|
|
1273
|
+
const value: u8[] = values[k]
|
|
1274
|
+
// One choice covers the name and the value, so Huffman is taken only
|
|
1275
|
+
// when it shortens the value and does not lengthen the name: neither
|
|
1276
|
+
// string is then longer than the bound above counted it.
|
|
1277
|
+
const name: u8[] = names[k]
|
|
1278
|
+
const huffman: boolean =
|
|
1279
|
+
hpackHuffmanLength(this.encoder.huffman, value, H2_ZERO, toI32(value.length)) < toI32(value.length) &&
|
|
1280
|
+
hpackHuffmanLength(this.encoder.huffman, name, H2_ZERO, toI32(name.length)) <= toI32(name.length)
|
|
1281
|
+
const indexing: i32 = httpFieldSensitive(name) ? HPACK_INDEX_NEVER : HPACK_INDEX_WITHOUT
|
|
1282
|
+
this.encoder.encodeField(this.encoded, name, value, indexing, huffman)
|
|
1283
|
+
}
|
|
1284
|
+
const total: i32 = toI32(this.encoded.length)
|
|
1285
|
+
let at: i32 = 0
|
|
1286
|
+
let first: boolean = true
|
|
1287
|
+
while (first || at < total) {
|
|
1288
|
+
const left: i32 = total - at
|
|
1289
|
+
const chunk: i32 = left < this.peerMaxFrameSize ? left : this.peerMaxFrameSize
|
|
1290
|
+
const last: boolean = at + chunk >= total
|
|
1291
|
+
const flags: i32 =
|
|
1292
|
+
(last ? H2_FLAG_END_HEADERS : H2_ZERO) | (first && endStream ? H2_FLAG_END_STREAM : H2_ZERO)
|
|
1293
|
+
this.wrote(
|
|
1294
|
+
first
|
|
1295
|
+
? http2WriteHeaders(this.output, this.outputEnd, s.id, this.encoded, at, chunk, flags, H2_NO_PAD)
|
|
1296
|
+
: http2WriteContinuation(this.output, this.outputEnd, s.id, this.encoded, at, chunk, flags)
|
|
1297
|
+
)
|
|
1298
|
+
at = at + chunk
|
|
1299
|
+
first = false
|
|
1300
|
+
}
|
|
1301
|
+
if (endStream) {
|
|
1302
|
+
this.localEnd(s)
|
|
1303
|
+
}
|
|
1304
|
+
return 0
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Answers stream `id` with `status` and the fields `names[k]`: `values[k]`,
|
|
1309
|
+
* lowercase and none of them connection-specific; END_STREAM when
|
|
1310
|
+
* `endStream`. A 1xx status but 101 may come before the final one and may
|
|
1311
|
+
* not end the stream. Answers as `sendHead` does.
|
|
1312
|
+
*/
|
|
1313
|
+
respond(id: i32, status: i32, names: u8[][], values: u8[][], endStream: boolean): i32 {
|
|
1314
|
+
const s: Http2Stream | null = this.writable(id)
|
|
1315
|
+
if (s === null) {
|
|
1316
|
+
return H2_CLOSED
|
|
1317
|
+
}
|
|
1318
|
+
const interim: boolean = status >= 100 && status < 200
|
|
1319
|
+
// 101 switches protocols, which HTTP/2 does not do (§8.6).
|
|
1320
|
+
if (status < 100 || status > 599 || status === 101 || s.responded || (interim && endStream)) {
|
|
1321
|
+
return H2_INVALID
|
|
1322
|
+
}
|
|
1323
|
+
const result: i32 = this.sendHead(s, status, names, values, endStream)
|
|
1324
|
+
if (result === 0 && !interim) {
|
|
1325
|
+
s.responded = true
|
|
1326
|
+
}
|
|
1327
|
+
return result
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** Ends stream `id` with a trailer section, after its response head (§8.1). Answers as `sendHead` does. */
|
|
1331
|
+
writeTrailers(id: i32, names: u8[][], values: u8[][]): i32 {
|
|
1332
|
+
const s: Http2Stream | null = this.writable(id)
|
|
1333
|
+
if (s === null) {
|
|
1334
|
+
return H2_CLOSED
|
|
1335
|
+
}
|
|
1336
|
+
if (!s.responded) {
|
|
1337
|
+
return H2_INVALID
|
|
1338
|
+
}
|
|
1339
|
+
return this.sendHead(s, H2_ZERO, names, values, true)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Sends as much of `buf[off, off + len)` on stream `id` as the stream's
|
|
1344
|
+
* window, the connection's window, and the output allow, in frames no
|
|
1345
|
+
* larger than the peer's SETTINGS_MAX_FRAME_SIZE, and answers how much.
|
|
1346
|
+
* END_STREAM goes on the last frame when `endStream` and every byte was
|
|
1347
|
+
* taken; `len` 0 with `endStream` sends an empty DATA that ends the stream.
|
|
1348
|
+
* Answers `H2_AGAIN` when nothing could be taken — after H2_WINDOW for the
|
|
1349
|
+
* stream or the connection, or once the output has drained, it may be
|
|
1350
|
+
* tried again — and `H2_CLOSED` for a stream with no final response head
|
|
1351
|
+
* or that has ended.
|
|
1352
|
+
*/
|
|
1353
|
+
writeData(id: i32, buf: u8[], off: i32, len: i32, endStream: boolean): i32 {
|
|
1354
|
+
http2CheckWindow("Http2Connection.writeData", buf, off, len)
|
|
1355
|
+
const s: Http2Stream | null = this.writable(id)
|
|
1356
|
+
if (s === null || !s.responded) {
|
|
1357
|
+
return H2_CLOSED
|
|
1358
|
+
}
|
|
1359
|
+
let sent: i32 = 0
|
|
1360
|
+
while (sent < len) {
|
|
1361
|
+
const room: i32 = this.bodyRoom() - H2_FRAME_HEADER_SIZE
|
|
1362
|
+
let chunk: i64 = Math.min(toI64(len - sent), toI64(this.peerMaxFrameSize))
|
|
1363
|
+
chunk = Math.min(chunk, Math.min(s.sendWindow, this.sendWindow))
|
|
1364
|
+
chunk = Math.min(chunk, toI64(room))
|
|
1365
|
+
if (chunk <= toI64(0)) {
|
|
1366
|
+
if (s.sendWindow <= toI64(0)) {
|
|
1367
|
+
s.blocked = true
|
|
1368
|
+
}
|
|
1369
|
+
if (this.sendWindow <= toI64(0)) {
|
|
1370
|
+
this.sendBlocked = true
|
|
1371
|
+
}
|
|
1372
|
+
break
|
|
1373
|
+
}
|
|
1374
|
+
const n: i32 = toI32(chunk)
|
|
1375
|
+
const last: boolean = endStream && sent + n === len
|
|
1376
|
+
this.wrote(
|
|
1377
|
+
http2WriteData(
|
|
1378
|
+
this.output,
|
|
1379
|
+
this.outputEnd,
|
|
1380
|
+
id,
|
|
1381
|
+
buf,
|
|
1382
|
+
off + sent,
|
|
1383
|
+
n,
|
|
1384
|
+
last ? H2_FLAG_END_STREAM : 0,
|
|
1385
|
+
H2_NO_PAD
|
|
1386
|
+
)
|
|
1387
|
+
)
|
|
1388
|
+
s.sendWindow = s.sendWindow - chunk
|
|
1389
|
+
this.sendWindow = this.sendWindow - chunk
|
|
1390
|
+
sent = sent + n
|
|
1391
|
+
}
|
|
1392
|
+
if (len === 0 && endStream) {
|
|
1393
|
+
if (this.bodyRoom() < H2_FRAME_HEADER_SIZE) {
|
|
1394
|
+
return H2_AGAIN
|
|
1395
|
+
}
|
|
1396
|
+
this.wrote(
|
|
1397
|
+
http2WriteData(this.output, this.outputEnd, id, buf, off, H2_ZERO, H2_FLAG_END_STREAM, H2_NO_PAD)
|
|
1398
|
+
)
|
|
1399
|
+
}
|
|
1400
|
+
if (endStream && sent === len) {
|
|
1401
|
+
this.localEnd(s)
|
|
1402
|
+
}
|
|
1403
|
+
return sent === 0 && len > 0 ? H2_AGAIN : sent
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Resets stream `id` with `code` (§6.4), from the program's side: CANCEL,
|
|
1408
|
+
* say. Answers 0, `H2_AGAIN` when the output has no room for the frame yet
|
|
1409
|
+
* (the stream stays open), or `H2_CLOSED`.
|
|
1410
|
+
*/
|
|
1411
|
+
reset(id: i32, code: i32): i32 {
|
|
1412
|
+
if (this.state === H2_STATE_FAILED || this.find(id) === null) {
|
|
1413
|
+
return H2_CLOSED
|
|
1414
|
+
}
|
|
1415
|
+
this.room()
|
|
1416
|
+
return this.resetStream(id, code) ? 0 : H2_AGAIN
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Starts a graceful close (§6.8): GOAWAY with NO_ERROR naming the last
|
|
1421
|
+
* stream the peer opened. Streams already open finish; newer ones are
|
|
1422
|
+
* ignored; `isDone` once the last has closed and everything is sent.
|
|
1423
|
+
* Answers 0, also when a GOAWAY was already sent or the connection has
|
|
1424
|
+
* failed, or `H2_AGAIN` when the output has no room for the frame yet.
|
|
1425
|
+
*/
|
|
1426
|
+
goaway(): i32 {
|
|
1427
|
+
if (this.state === H2_STATE_FAILED || this.goawaySent) {
|
|
1428
|
+
return 0
|
|
1429
|
+
}
|
|
1430
|
+
this.room()
|
|
1431
|
+
return this.sendGoaway(H2_NO_ERROR) ? 0 : H2_AGAIN
|
|
1432
|
+
}
|
|
1433
|
+
}
|