@amritk/nish-x86_64-linux 0.23.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/bin/nish +0 -0
  2. package/docs/AI.md +50 -23
  3. package/package.json +1 -1
  4. package/runtime/browser.mjs +4 -3
  5. package/runtime/core/arrays.d.ts +0 -2
  6. package/runtime/core/arrays.mjs +0 -2
  7. package/runtime/core/host.d.ts +8 -0
  8. package/runtime/core/host.mjs +50 -2
  9. package/runtime/core/numbers.mjs +2 -3
  10. package/runtime/nish.d.ts +100 -4
  11. package/runtime/nish.h +34 -0
  12. package/runtime/nish.mjs +23 -5
  13. package/runtime/node.mjs +57 -6
  14. package/runtime/runtime-net.c +45 -4
  15. package/runtime/runtime-os.c +115 -12
  16. package/runtime/runtime-term.c +2 -2
  17. package/runtime/runtime.c +1 -1
  18. package/scripts/build.sh +13 -3
  19. package/std/README.md +44 -7
  20. package/std/cli-args.ts +1216 -0
  21. package/std/cli-flags.ts +585 -0
  22. package/std/cli.ts +308 -19
  23. package/std/compress/checksum.ts +290 -0
  24. package/std/compress/deflate.ts +2790 -0
  25. package/std/compress/gzip.ts +761 -0
  26. package/std/compress/zlib.ts +168 -0
  27. package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
  28. package/std/crypto/p256.ts +63 -1
  29. package/std/crypto/p384.ts +6044 -0
  30. package/std/crypto/pki-roots.ts +322 -0
  31. package/std/crypto/pki.ts +1207 -0
  32. package/std/crypto/rsa.ts +663 -0
  33. package/std/crypto/sha256.ts +2 -3
  34. package/std/crypto/x25519.ts +3 -4
  35. package/std/crypto/x509.ts +1054 -58
  36. package/std/net/dns-config.ts +734 -0
  37. package/std/net/dns-message.ts +517 -0
  38. package/std/net/dns-resolver.ts +1015 -0
  39. package/std/net/dns.ts +478 -0
  40. package/std/net/http-compress.ts +393 -0
  41. package/std/net/http1-client.ts +1227 -0
  42. package/std/net/http1-server.ts +101 -1
  43. package/std/net/http1.ts +9 -9
  44. package/std/net/tls/client.ts +1094 -0
  45. package/std/net/tls/codec.ts +585 -3
  46. package/std/net/tls/record-client.ts +1192 -0
  47. package/std/net/tls/verify.ts +221 -0
  48. package/std/net/tls-tcp.ts +642 -11
  49. package/std/net/tls.ts +2 -2
  50. package/std/net/url.ts +559 -4
  51. package/std/yaml-write.ts +415 -0
@@ -0,0 +1,1227 @@
1
+ /**
2
+ * `nish/net/http1-client` — the client side of HTTP/1.1, sans-IO: a request
3
+ * writer and an incremental response parser, bytes in, events and bytes out,
4
+ * and no socket anywhere. `nish/net/http1` is the server side; the event
5
+ * constants and the chunk writers are shared and imported from there.
6
+ *
7
+ * **The writer** is `http1RequestHead`, which answers a request line and
8
+ * header section with the framing field the body needs, and
9
+ * `http1WriteRequestHead`, which writes the same head into a caller's buffer
10
+ * with nothing allocated. A chunked body is written with `http1Chunk` and
11
+ * `http1LastChunk` from `nish/net/http1`. The writer refuses anything that
12
+ * would let a caller's string end a line early or frame the body itself — the
13
+ * request-splitting and request-smuggling shapes.
14
+ *
15
+ * **The parser** is a push-pull machine, like the server's. A caller tells it
16
+ * what it asked (`expect`), `feed`s whatever slice the socket gave it, cut
17
+ * anywhere, and calls `next()` until it answers `HTTP1_NEED_MORE`:
18
+ *
19
+ * import { HTTP1_NEED_MORE, HTTP1_HEAD, HTTP1_BODY, HTTP1_END, HTTP1_ERROR } from "nish/net/http1";
20
+ * import { Http1ResponseParser } from "nish/net/http1-client";
21
+ *
22
+ * const p = new Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes);
23
+ * p.expect(false, false);
24
+ * p.feed(received, 0, n); // and p.finish() when the server closes
25
+ * let event: i32 = p.next();
26
+ * while (event !== HTTP1_NEED_MORE) {
27
+ * // HTTP1_HEAD: p.status, p.reason, p.header("content-type"), ...
28
+ * // HTTP1_BODY: p.body[p.bodyOff .. p.bodyOff + p.bodyLen)
29
+ * // HTTP1_ERROR: p.error and p.errorText say why; close
30
+ * event = p.next();
31
+ * }
32
+ *
33
+ * The events are `HTTP1_INFORMATIONAL` (an interim 1xx response was read; the
34
+ * final one follows), `HTTP1_HEAD` (the final response's status line and
35
+ * header section are in), `HTTP1_BODY` (some body bytes, de-chunked),
36
+ * `HTTP1_END` (the response is complete; with `keepAlive()` the next one
37
+ * follows), `HTTP1_UPGRADE` (a CONNECT was answered with 2xx and the
38
+ * connection is a tunnel now: `tunnelBytes()` answers what was fed past the
39
+ * head), `HTTP1_CLOSED` (the connection is done: the server closed it
40
+ * between responses, or the last response was not kept alive) and
41
+ * `HTTP1_ERROR`. The last three repeat on every later call. Only complete
42
+ * lines are acted on, body bytes are counted, never guessed, and every
43
+ * refusal is decided by the same byte wherever the input was cut, so the
44
+ * sequence of events, the concatenated body and the error code do not depend
45
+ * on the split —
46
+ * `tests/link/net_http1_client` feeds a corpus at every split point.
47
+ *
48
+ * **How a response ends** (RFC 9112 §6.3): a response to HEAD, a 1xx, 204 or
49
+ * 304 has no body whatever its fields say; a 2xx to CONNECT is a tunnel; a
50
+ * `Transfer-Encoding: chunked` response is de-chunked; a `Content-Length`
51
+ * response is that many bytes; anything else is read until the server closes,
52
+ * which `finish()` reports. A connection closed anywhere else is
53
+ * `HTTP1_E_TRUNCATED`, so a cut-short body is never taken for a whole one.
54
+ *
55
+ * **What it refuses**, each with its own code in `error`: a malformed status
56
+ * line, an HTTP major version other than 1, a bare LF, a malformed field line
57
+ * (an obs-fold, whitespace before the colon, a control byte in a value), a 101
58
+ * (this client never asks to switch protocols), `Transfer-Encoding` together
59
+ * with `Content-Length`, a transfer coding other than one `chunked` or one in
60
+ * an HTTP/1.0 response, a `Content-Length` that is not a decimal or whose
61
+ * repeats disagree, a malformed chunk, a status line, header section, field
62
+ * count, body or trailer section over the caller's limit, a chunk-size line
63
+ * over `HTTP1_MAX_CHUNK_LINE` and more than `HTTP1_MAX_INFORMATIONAL` interim
64
+ * responses. Each of these is a response-smuggling or a resource shape, and
65
+ * each is refused rather than repaired: a client that repairs one differently
66
+ * from a proxy disagrees with it about where the next response starts. An
67
+ * error is final.
68
+ *
69
+ * Private helpers share the importing program's flat symbol namespace
70
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
71
+ *
72
+ * Written from RFC 9112 and RFC 9110, not ported from another implementation.
73
+ */
74
+ import { copyBytes } from "nish/bytes"
75
+ import {
76
+ HTTP1_BODY,
77
+ HTTP1_CLOSED,
78
+ HTTP1_END,
79
+ HTTP1_ERROR,
80
+ HTTP1_HEAD,
81
+ HTTP1_NEED_MORE,
82
+ HTTP1_NO_ROOM,
83
+ HTTP1_REFUSED,
84
+ HTTP1_UPGRADE,
85
+ HTTP1_FIRST_INPUT,
86
+ http1HexValue,
87
+ http1IsFieldText,
88
+ http1IsOws,
89
+ http1IsToken,
90
+ http1IsTokenByte,
91
+ http1IsValueByte,
92
+ http1PutText,
93
+ http1SameWord,
94
+ http1Text,
95
+ } from "nish/net/http1"
96
+
97
+ /** `next()`: an interim 1xx response was read; `status` and the fields are its. */
98
+ export const HTTP1_INFORMATIONAL: i32 = 7
99
+
100
+ /** The longest chunk-size line, extension included, the parser reads. */
101
+ export const HTTP1_MAX_CHUNK_LINE: i32 = 4096
102
+ /** The most interim 1xx responses the parser reads before a final one. */
103
+ export const HTTP1_MAX_INFORMATIONAL: i32 = 32
104
+
105
+ /** `error`: nothing is wrong. */
106
+ export const HTTP1_E_NONE: i32 = 0
107
+ /** `error`: the status line is not `HTTP/1.x`, a three-digit status from 100 to 599 and an optional reason. */
108
+ export const HTTP1_E_STATUS_LINE: i32 = 1
109
+ /** `error`: the response's HTTP major version is not 1. */
110
+ export const HTTP1_E_VERSION: i32 = 2
111
+ /** `error`: a line ended in a LF without its CR (RFC 9112 §2.2). */
112
+ export const HTTP1_E_BARE_LF: i32 = 3
113
+ /** `error`: a malformed field line: an obs-fold, a bad name, whitespace before the colon, a control byte. */
114
+ export const HTTP1_E_FIELD: i32 = 4
115
+ /** `error`: a 101, which this client never asks for. */
116
+ export const HTTP1_E_SWITCH: i32 = 5
117
+ /** `error`: both `Transfer-Encoding` and `Content-Length` (RFC 9112 §6.1). */
118
+ export const HTTP1_E_FRAMING: i32 = 6
119
+ /** `error`: a transfer coding list that is not one `chunked`, or one in an HTTP/1.0 response. */
120
+ export const HTTP1_E_CODING: i32 = 7
121
+ /** `error`: a `Content-Length` that is not a run of digits, or values that differ (a value past 2^53 differs from any). */
122
+ export const HTTP1_E_LENGTH: i32 = 8
123
+ /** `error`: a malformed chunk size, chunk extension or chunk terminator (RFC 9112 §7.1). */
124
+ export const HTTP1_E_CHUNK: i32 = 9
125
+ /** `error`: the connection closed before the response ended. */
126
+ export const HTTP1_E_TRUNCATED: i32 = 10
127
+ /** `error`: a status line longer than `maxStatusLine`. */
128
+ export const HTTP1_E_STATUS_LINE_LIMIT: i32 = 11
129
+ /** `error`: a header section longer than `maxHeaderBytes`. */
130
+ export const HTTP1_E_HEADER_LIMIT: i32 = 12
131
+ /** `error`: more fields, trailers included, than `maxHeaders`. */
132
+ export const HTTP1_E_COUNT_LIMIT: i32 = 13
133
+ /** `error`: a declared or a received body longer than `maxBody`. */
134
+ export const HTTP1_E_BODY_LIMIT: i32 = 14
135
+ /** `error`: a trailer section longer than `maxTrailerBytes`. */
136
+ export const HTTP1_E_TRAILER_LIMIT: i32 = 15
137
+ /** `error`: a chunk-size line longer than `HTTP1_MAX_CHUNK_LINE`. */
138
+ export const HTTP1_E_CHUNK_LINE_LIMIT: i32 = 16
139
+ /** `error`: more than `HTTP1_MAX_INFORMATIONAL` interim responses. */
140
+ export const HTTP1_E_INFORMATIONAL_LIMIT: i32 = 17
141
+
142
+ // --- The writer -------------------------------------------------------------
143
+
144
+ /** How many decimal digits `n`, which is not negative, takes. */
145
+ const http1ClientDigits = (n: i64): i32 => {
146
+ let digits: i32 = 1
147
+ let v: i64 = n
148
+ while (v >= toI64(10)) {
149
+ v = v / toI64(10)
150
+ digits += 1
151
+ }
152
+ return digits
153
+ }
154
+
155
+ /** Writes the decimal digits of `n`, which is not negative, at `out[at ..)`, which holds them, and answers where they end. */
156
+ const http1ClientPutDecimal = (out: u8[], at: i32, n: i64): i32 => {
157
+ const digits: i32 = http1ClientDigits(n)
158
+ let v: i64 = n
159
+ for (let k: i32 = at + digits - 1; k >= at && k >= 0 && k < toI32(out.length); k -= 1) {
160
+ out[k] = toU8(toI32(v % toI64(10)) + 48)
161
+ v = v / toI64(10)
162
+ }
163
+ return at + digits
164
+ }
165
+
166
+ /** Whether `c` may follow the first letter of a URI scheme (RFC 3986 §3.1). */
167
+ const http1ClientIsSchemeByte = (c: i32): boolean =>
168
+ (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 43 || c === 45 || c === 46
169
+
170
+ /**
171
+ * Whether `target` is a request target `method` may send (RFC 9112 §3.2):
172
+ * authority-form (`host:port`) for CONNECT and for nothing else, `*` for
173
+ * OPTIONS only, and otherwise origin-form (`/path?query`) or absolute-form
174
+ * (`scheme:...`). A `host:port` sent with another method is well-formed
175
+ * absolute-form, the host its scheme, so it is not refused. No form holds a
176
+ * byte at or below SP, DEL, a non-ASCII byte or a `#`: a fragment is never
177
+ * sent, and the others would end the target or the line early.
178
+ */
179
+ const http1ClientTargetOk = (method: string, target: string): boolean => {
180
+ const n: i32 = toI32(target.length)
181
+ if (n === 0) {
182
+ return false
183
+ }
184
+ for (let k: i32 = 0; k < n; k += 1) {
185
+ const c: i32 = toI32(target.charCodeAt(k))
186
+ if (c <= 32 || c >= 127 || c === 35) {
187
+ return false
188
+ }
189
+ }
190
+ if (method === "CONNECT") {
191
+ // uri-host ":" port, with no path, query or userinfo; an IPv6 host is
192
+ // bracketed, so the port follows the last colon.
193
+ let colon: i32 = -1
194
+ for (let k: i32 = 0; k < n; k += 1) {
195
+ const c: i32 = toI32(target.charCodeAt(k))
196
+ if (c === 47 || c === 63 || c === 64) {
197
+ return false
198
+ }
199
+ if (c === 58) {
200
+ colon = k
201
+ }
202
+ }
203
+ if (colon < 1 || n - colon - 1 < 1 || n - colon - 1 > 5) {
204
+ return false
205
+ }
206
+ for (let k: i32 = colon + 1; k < n; k += 1) {
207
+ const c: i32 = toI32(target.charCodeAt(k))
208
+ if (c < 48 || c > 57) {
209
+ return false
210
+ }
211
+ }
212
+ return true
213
+ }
214
+ const first: i32 = toI32(target.charCodeAt(0))
215
+ if (first === 42) {
216
+ return n === 1 && method === "OPTIONS"
217
+ }
218
+ if (first === 47) {
219
+ return true
220
+ }
221
+ // absolute-form: a scheme, a letter then scheme bytes, and its colon.
222
+ if (!((first >= 97 && first <= 122) || (first >= 65 && first <= 90))) {
223
+ return false
224
+ }
225
+ for (let k: i32 = 1; k < n; k += 1) {
226
+ const c: i32 = toI32(target.charCodeAt(k))
227
+ if (c === 58) {
228
+ return true
229
+ }
230
+ if (!http1ClientIsSchemeByte(c)) {
231
+ return false
232
+ }
233
+ }
234
+ return false
235
+ }
236
+
237
+ /** Whether `value[k]` is a comma, the list separator of RFC 9110 §5.6.1. */
238
+ const http1ClientIsComma = (value: string, k: i32): boolean =>
239
+ k >= 0 && k < toI32(value.length) && toI32(value.charCodeAt(k)) === 44
240
+
241
+ /** Where `value[a .. b)` starts once its leading OWS is dropped. */
242
+ const http1ClientTrimStart = (value: string, a: i32, b: i32): i32 => {
243
+ let i: i32 = a
244
+ while (i >= 0 && i < b && i < toI32(value.length) && http1IsOws(toI32(value.charCodeAt(i)))) {
245
+ i += 1
246
+ }
247
+ return i
248
+ }
249
+
250
+ /** Where `value[a .. b)` ends once its trailing OWS is dropped. */
251
+ const http1ClientTrimEnd = (value: string, a: i32, b: i32): i32 => {
252
+ let i: i32 = b
253
+ while (i > a && i > 0 && i <= toI32(value.length)) {
254
+ const j: i32 = i - 1
255
+ if (j < 0 || j >= toI32(value.length) || !http1IsOws(toI32(value.charCodeAt(j)))) {
256
+ return i
257
+ }
258
+ i = j
259
+ }
260
+ return i
261
+ }
262
+
263
+ /**
264
+ * Whether the comma-separated list in `value` (RFC 9110 §5.6.1) holds `word`,
265
+ * which is lowercase, in any case, each element trimmed of OWS.
266
+ */
267
+ const http1ClientListHas = (value: string, word: string): boolean => {
268
+ const n: i32 = toI32(value.length)
269
+ let start: i32 = 0
270
+ for (let k: i32 = 0; k <= n; k += 1) {
271
+ if (k === n || http1ClientIsComma(value, k)) {
272
+ const a: i32 = http1ClientTrimStart(value, start, k)
273
+ const b: i32 = http1ClientTrimEnd(value, a, k)
274
+ if (http1SameWord(value, a, b, word)) {
275
+ return true
276
+ }
277
+ start = k + 1
278
+ }
279
+ }
280
+ return false
281
+ }
282
+
283
+ /**
284
+ * The bytes `http1WriteRequestHead` writes for these arguments, counted in 64
285
+ * bits so that no caller's lengths can wrap the sum, or -1 for a head it
286
+ * refuses. Every check is made here, before anything is written.
287
+ */
288
+ const http1ClientRequestNeed = (
289
+ method: string,
290
+ target: string,
291
+ names: string[],
292
+ values: string[],
293
+ bodyLength: i64
294
+ ): i64 => {
295
+ if (!http1IsToken(method) || !http1ClientTargetOk(method, target)) {
296
+ return toI64(-1)
297
+ }
298
+ if (bodyLength < toI64(-2)) {
299
+ return toI64(-1)
300
+ }
301
+ const count: i32 = toI32(names.length)
302
+ if (toI32(values.length) !== count) {
303
+ return toI64(-1)
304
+ }
305
+ // method SP target SP "HTTP/1.1" CRLF, the fields, the framing field and
306
+ // the empty line.
307
+ let need: i64 = toI64(method.length) + toI64(target.length) + toI64(14)
308
+ let hosts: i32 = 0
309
+ for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
310
+ const name: string = names[i]
311
+ const value: string = values[i]
312
+ const nameLength: i32 = toI32(name.length)
313
+ if (!http1IsToken(name) || !http1IsFieldText(value)) {
314
+ return toI64(-1)
315
+ }
316
+ if (
317
+ http1SameWord(name, 0, nameLength, "content-length") ||
318
+ http1SameWord(name, 0, nameLength, "transfer-encoding")
319
+ ) {
320
+ return toI64(-1)
321
+ }
322
+ if (http1SameWord(name, 0, nameLength, "connection") && http1ClientListHas(value, "upgrade")) {
323
+ return toI64(-1)
324
+ }
325
+ if (http1SameWord(name, 0, nameLength, "host")) {
326
+ hosts += 1
327
+ }
328
+ need = need + toI64(nameLength) + toI64(value.length) + toI64(4)
329
+ }
330
+ if (hosts !== 1) {
331
+ return toI64(-1)
332
+ }
333
+ if (bodyLength >= toI64(0)) {
334
+ need = need + toI64(18 + http1ClientDigits(bodyLength))
335
+ } else if (bodyLength === toI64(-1)) {
336
+ need = need + toI64(28)
337
+ }
338
+ return need
339
+ }
340
+
341
+ /** Writes field `i` at `out[at ..)`, which holds it, and answers where it ends. */
342
+ const http1ClientPutField = (out: u8[], at: i32, names: string[], values: string[], i: i32): i32 => {
343
+ if (i < 0 || i >= toI32(names.length) || i >= toI32(values.length)) {
344
+ return at
345
+ }
346
+ let end: i32 = http1PutText(out, at, names[i])
347
+ end = http1PutText(out, end, ": ")
348
+ end = http1PutText(out, end, values[i])
349
+ return http1PutText(out, end, "\r\n")
350
+ }
351
+
352
+ /**
353
+ * An HTTP/1.1 request line and header section (RFC 9112 §3, §5), ending in
354
+ * the empty line, written into `out` at `at` with nothing allocated, and
355
+ * answers where it ends. `Host` goes first, as RFC 9110 §7.2 asks, then the
356
+ * caller's other fields in order, then the framing field `bodyLength` asks
357
+ * for: `Content-Length: n` for `n >= 0`, `Transfer-Encoding: chunked` for -1
358
+ * (`HTTP1_CHUNKED`), and neither for -2 (`HTTP1_NO_BODY`).
359
+ *
360
+ * Answers `HTTP1_REFUSED` for a method that is not a token; a target that is
361
+ * not a form the method may send (see the guide) or that holds a space, a
362
+ * control byte, a non-ASCII byte or a `#`; a field name that is not a token or
363
+ * a value holding CR, LF or another control byte (which would end the line
364
+ * early and let the rest be read as a field or a second request); `names` and
365
+ * `values` of different lengths; not exactly one `Host`; a `Content-Length` or
366
+ * `Transfer-Encoding` among the caller's fields, since the framing is decided
367
+ * here; a `Connection` that lists `upgrade`, which the parser refuses the
368
+ * answer to; and a `bodyLength` below -2. Answers `HTTP1_NO_ROOM` when it is
369
+ * well formed but does not fit. Either way `out` is untouched.
370
+ */
371
+ export const http1WriteRequestHead = (
372
+ out: u8[],
373
+ at: i32,
374
+ method: string,
375
+ target: string,
376
+ names: string[],
377
+ values: string[],
378
+ bodyLength: i64
379
+ ): i32 => {
380
+ const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength)
381
+ if (need < toI64(0)) {
382
+ return HTTP1_REFUSED
383
+ }
384
+ if (at < 0 || need > toI64(toI32(out.length) - at)) {
385
+ return HTTP1_NO_ROOM
386
+ }
387
+ return http1ClientPutHead(out, at, method, target, names, values, bodyLength)
388
+ }
389
+
390
+ /** Writes a head `http1ClientRequestNeed` accepted into room it counted; answers the end. */
391
+ const http1ClientPutHead = (
392
+ out: u8[],
393
+ at: i32,
394
+ method: string,
395
+ target: string,
396
+ names: string[],
397
+ values: string[],
398
+ bodyLength: i64
399
+ ): i32 => {
400
+ let end: i32 = http1PutText(out, at, method)
401
+ end = http1PutText(out, end, " ")
402
+ end = http1PutText(out, end, target)
403
+ end = http1PutText(out, end, " HTTP/1.1\r\n")
404
+ const count: i32 = toI32(names.length)
405
+ for (let i: i32 = 0; i < count; i += 1) {
406
+ if (http1SameWord(names[i], 0, toI32(names[i].length), "host")) {
407
+ end = http1ClientPutField(out, end, names, values, i)
408
+ }
409
+ }
410
+ for (let i: i32 = 0; i < count; i += 1) {
411
+ if (!http1SameWord(names[i], 0, toI32(names[i].length), "host")) {
412
+ end = http1ClientPutField(out, end, names, values, i)
413
+ }
414
+ }
415
+ if (bodyLength >= toI64(0)) {
416
+ end = http1PutText(out, end, "Content-Length: ")
417
+ end = http1ClientPutDecimal(out, end, bodyLength)
418
+ end = http1PutText(out, end, "\r\n")
419
+ } else if (bodyLength === toI64(-1)) {
420
+ end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
421
+ }
422
+ return http1PutText(out, end, "\r\n")
423
+ }
424
+
425
+ /**
426
+ * `http1WriteRequestHead`'s head in an array of its own, or `null` for
427
+ * everything it refuses.
428
+ */
429
+ export const http1RequestHead = (
430
+ method: string,
431
+ target: string,
432
+ names: string[],
433
+ values: string[],
434
+ bodyLength: i64
435
+ ): u8[] | null => {
436
+ const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength)
437
+ if (need < toI64(0) || need > toI64(1073741824)) {
438
+ return null
439
+ }
440
+ const out: u8[] = new Array<u8>(toI32(need))
441
+ http1ClientPutHead(out, 0, method, target, names, values, bodyLength)
442
+ return out
443
+ }
444
+
445
+ // --- The parser -------------------------------------------------------------
446
+
447
+ // The parser's states, one per kind of thing it is waiting for.
448
+ const HTTP1_CLIENT_S_STATUS_LINE: i32 = 0
449
+ const HTTP1_CLIENT_S_FIELDS: i32 = 1
450
+ const HTTP1_CLIENT_S_LENGTH_BODY: i32 = 2
451
+ const HTTP1_CLIENT_S_CLOSE_BODY: i32 = 3
452
+ const HTTP1_CLIENT_S_CHUNK_SIZE: i32 = 4
453
+ const HTTP1_CLIENT_S_CHUNK_DATA: i32 = 5
454
+ const HTTP1_CLIENT_S_CHUNK_CRLF: i32 = 6
455
+ const HTTP1_CLIENT_S_TRAILERS: i32 = 7
456
+ const HTTP1_CLIENT_S_MESSAGE_END: i32 = 8
457
+ const HTTP1_CLIENT_S_TUNNEL: i32 = 9
458
+ const HTTP1_CLIENT_S_CLOSED: i32 = 10
459
+ const HTTP1_CLIENT_S_ERROR: i32 = 11
460
+
461
+ /** `http1ClientTakeLine`'s answer when a whole line is ready; never an event. */
462
+ const HTTP1_CLIENT_LINE: i32 = -1
463
+
464
+ /** The largest `maxBody`: 2^53, so that sums of it cannot overflow an `i64`. */
465
+ const HTTP1_CLIENT_MAX_BODY: i64 = 9007199254740992
466
+
467
+ /** `limit` clamped into 0 to 2^30. */
468
+ const http1ClientClamp = (limit: i32): i32 => {
469
+ if (limit < 0) {
470
+ return 0
471
+ }
472
+ return limit > 1073741824 ? 1073741824 : limit
473
+ }
474
+
475
+ /**
476
+ * One connection's response parser: responses on a kept-alive connection
477
+ * follow one another through the same parser, each after its `expect`.
478
+ */
479
+ export class Http1ResponseParser {
480
+ // --- the response: valid from its HEAD or INFORMATIONAL until the next ----
481
+ /** The reason phrase, which RFC 9112 §4 says a client ignores; for a log. */
482
+ reason: string = ""
483
+ /** The field names, in lowercase, in the order they arrived; `values` beside them, OWS trimmed. */
484
+ names: string[]
485
+ values: string[]
486
+ /** The buffer the last `HTTP1_BODY`'s bytes are in, at `bodyOff`, `bodyLen` long. */
487
+ body: u8[]
488
+ /** What was wrong, once `next()` said `HTTP1_ERROR`; for a log. */
489
+ errorText: string = ""
490
+ /** The bytes fed and not yet consumed are `buf[start .. end)`. */
491
+ buf: u8[]
492
+
493
+ // --- limits, the caller's --------------------------------------------------
494
+ maxBody: i64 = 0
495
+ /** The declared body length, or -1 when the response has none or is chunked or read until close. */
496
+ contentLength: i64 = -1
497
+ /** Bytes left in a length-delimited body or in the current chunk. */
498
+ left: i64 = 0
499
+ /** De-chunked body bytes so far, against `maxBody`. */
500
+ bodyTotal: i64 = 0
501
+ maxStatusLine: i32 = 0
502
+ maxHeaderBytes: i32 = 0
503
+ maxHeaders: i32 = 0
504
+ maxTrailerBytes: i32 = 0
505
+
506
+ /** The status code of the response last read. */
507
+ status: i32 = 0
508
+ /** The minor version: 0 for HTTP/1.0, 1 for HTTP/1.1 and anything above it. */
509
+ minor: i32 = 1
510
+ bodyOff: i32 = 0
511
+ bodyLen: i32 = 0
512
+ /** Why `next()` said `HTTP1_ERROR`: one of the `HTTP1_E_` codes. */
513
+ error: i32 = 0
514
+
515
+ // --- internal --------------------------------------------------------------
516
+ start: i32 = 0
517
+ end: i32 = 0
518
+ /** Where the search for the next LF resumes, so a line fed a byte at a time is scanned once. */
519
+ scan: i32 = 0
520
+ state: i32 = 0
521
+ /** Bytes of the header or trailer section so far, against its limit. */
522
+ sectionBytes: i32 = 0
523
+ /** Field lines of the response so far, its trailers included, against `maxHeaders`. */
524
+ fieldCount: i32 = 0
525
+ /** Interim responses before this request's final one, against `HTTP1_MAX_INFORMATIONAL`. */
526
+ informational: i32 = 0
527
+ /** The line `http1ClientTakeLine` found is `buf[lineStart .. lineEnd)`, its CRLF dropped. */
528
+ lineStart: i32 = 0
529
+ lineEnd: i32 = 0
530
+
531
+ chunked: boolean = false
532
+ persistent: boolean = false
533
+ /** What `expect` said of the request this response answers. */
534
+ wasHead: boolean = false
535
+ wasConnect: boolean = false
536
+ /** Whether `finish()` said the server closed the connection. */
537
+ closed: boolean = false
538
+
539
+ /**
540
+ * Each `i32` limit is clamped into 0 to 2^30 and `maxBody` into 0 to 2^53,
541
+ * so that the sums the parser makes of them cannot overflow.
542
+ */
543
+ constructor(maxStatusLine: i32, maxHeaderBytes: i32, maxHeaders: i32, maxBody: i64, maxTrailerBytes: i32) {
544
+ this.maxStatusLine = http1ClientClamp(maxStatusLine)
545
+ this.maxHeaderBytes = http1ClientClamp(maxHeaderBytes)
546
+ this.maxHeaders = http1ClientClamp(maxHeaders)
547
+ this.maxTrailerBytes = http1ClientClamp(maxTrailerBytes)
548
+ let body: i64 = maxBody
549
+ if (body < toI64(0)) {
550
+ body = toI64(0)
551
+ }
552
+ this.maxBody = body > HTTP1_CLIENT_MAX_BODY ? HTTP1_CLIENT_MAX_BODY : body
553
+ this.buf = []
554
+ this.body = this.buf
555
+ this.names = []
556
+ this.values = []
557
+ }
558
+
559
+ /**
560
+ * Says what the request this response answers was: a response to HEAD has
561
+ * no body, and a 2xx to CONNECT is a tunnel. It takes effect only while the
562
+ * parser waits for a response's first status line, and the parser forgets it
563
+ * after that response's `HTTP1_END`; anywhere else it does nothing, so a
564
+ * pipelining caller says it once per response, after the last one's end.
565
+ */
566
+ expect(methodWasHead: boolean, wasConnect: boolean): void {
567
+ if (this.state === HTTP1_CLIENT_S_STATUS_LINE && this.informational === 0) {
568
+ this.wasHead = methodWasHead
569
+ this.wasConnect = wasConnect
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Appends `data[off .. off + len)` to the input. A window outside `data`
575
+ * panics. After `finish()`, or once the parser has closed or refused, the
576
+ * input is dropped; in a tunnel it is kept for `tunnelBytes()`.
577
+ */
578
+ feed(data: u8[], off: i32, len: i32): void {
579
+ const size: i32 = toI32(data.length)
580
+ if (off < 0 || len < 0 || off > size || len > size - off) {
581
+ panic("Http1ResponseParser: the window is outside the buffer")
582
+ }
583
+ if (this.closed || this.state === HTTP1_CLIENT_S_CLOSED || this.state === HTTP1_CLIENT_S_ERROR) {
584
+ return
585
+ }
586
+ http1ClientReserve(this, len)
587
+ const at: i32 = this.end
588
+ copyBytes(this.buf, at, data, off, len)
589
+ this.end = at + len
590
+ }
591
+
592
+ /**
593
+ * Says the server closed the connection: a body read until close ends
594
+ * there, a parser waiting for a response with nothing fed is closed, and
595
+ * anything else is `HTTP1_E_TRUNCATED` once the bytes fed before it are read.
596
+ */
597
+ finish(): void {
598
+ this.closed = true
599
+ }
600
+
601
+ /** The next event the input holds; see the module comment. */
602
+ next(): i32 {
603
+ const event: i32 = http1ClientNext(this)
604
+ if (event !== HTTP1_NEED_MORE || !this.closed) {
605
+ return event
606
+ }
607
+ if (this.state === HTTP1_CLIENT_S_CLOSE_BODY) {
608
+ this.state = HTTP1_CLIENT_S_MESSAGE_END
609
+ return http1ClientNext(this)
610
+ }
611
+ if (this.state === HTTP1_CLIENT_S_STATUS_LINE && this.informational === 0 && this.end === this.start) {
612
+ return HTTP1_CLOSED
613
+ }
614
+ return http1ClientFail(this, HTTP1_E_TRUNCATED, "the connection closed before the response ended")
615
+ }
616
+
617
+ /**
618
+ * Whether the connection may carry the next request once this response has
619
+ * ended (RFC 9112 §9.3): a 1.1 response unless `Connection` lists `close`,
620
+ * a 1.0 response only when it lists `keep-alive`, and never one read until
621
+ * close, a tunnel or an error.
622
+ */
623
+ keepAlive(): boolean {
624
+ return this.persistent
625
+ }
626
+
627
+ /** The value of the first field named `name` (any case), or `null`. */
628
+ header(name: string): string | null {
629
+ const names: string[] = this.names
630
+ const n: i32 = toI32(name.length)
631
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(this.values.length); i += 1) {
632
+ if (http1SameWord(name, 0, n, names[i])) {
633
+ return this.values[i]
634
+ }
635
+ }
636
+ return null
637
+ }
638
+
639
+ /** How many bytes were fed and not consumed yet. */
640
+ buffered(): i32 {
641
+ return this.end - this.start
642
+ }
643
+
644
+ /** In a tunnel: every byte fed past the response head, as a fresh array. */
645
+ tunnelBytes(): u8[] {
646
+ let n: i32 = this.end - this.start
647
+ if (n < 0 || this.state !== HTTP1_CLIENT_S_TUNNEL) {
648
+ n = 0
649
+ }
650
+ const out: u8[] = new Array<u8>(n)
651
+ if (n > 0) {
652
+ copyBytes(out, 0, this.buf, this.start, n)
653
+ }
654
+ return out
655
+ }
656
+ }
657
+
658
+ /** Makes room for `len` more bytes at `p.end`, moving the live bytes down first. */
659
+ const http1ClientReserve = (p: Http1ResponseParser, len: i32): void => {
660
+ const old: u8[] = p.buf
661
+ const capacity: i32 = toI32(old.length)
662
+ if (len <= capacity - p.end) {
663
+ return
664
+ }
665
+ const live: i32 = p.end - p.start
666
+ let size: i32 = capacity < HTTP1_FIRST_INPUT ? HTTP1_FIRST_INPUT : capacity
667
+ while (size - live < len) {
668
+ if (size > 1073741823) {
669
+ panic("Http1ResponseParser: more than 2^30 bytes buffered")
670
+ }
671
+ size = size * 2
672
+ }
673
+ // A fresh array when it has to grow, and the same one otherwise: either way
674
+ // the live bytes move to the front, and every index into them moves too.
675
+ const buf: u8[] = size === capacity ? old : new Array<u8>(size)
676
+ const from: i32 = p.start
677
+ if (live > 0) {
678
+ copyBytes(buf, 0, old, from, live)
679
+ }
680
+ p.buf = buf
681
+ p.scan -= from
682
+ p.start = 0
683
+ p.end = live
684
+ }
685
+
686
+ /**
687
+ * Refuses the input with `code` and drops what is buffered: nothing after a
688
+ * refusal is read. Answers `HTTP1_ERROR`, so a caller can `return` it.
689
+ */
690
+ const http1ClientFail = (p: Http1ResponseParser, code: i32, text: string): i32 => {
691
+ p.state = HTTP1_CLIENT_S_ERROR
692
+ p.start = p.end
693
+ p.error = code
694
+ p.errorText = text
695
+ p.persistent = false
696
+ return HTTP1_ERROR
697
+ }
698
+
699
+ /** Clears the per-response fields for the status line just taken. */
700
+ const http1ClientStartHead = (p: Http1ResponseParser): void => {
701
+ p.status = 0
702
+ p.reason = ""
703
+ p.minor = 1
704
+ p.names = []
705
+ p.values = []
706
+ p.contentLength = toI64(-1)
707
+ p.chunked = false
708
+ p.persistent = false
709
+ p.bodyLen = 0
710
+ p.bodyTotal = toI64(0)
711
+ p.sectionBytes = 0
712
+ p.fieldCount = 0
713
+ }
714
+
715
+ /**
716
+ * Finds the next line: answers `HTTP1_CLIENT_LINE` with it in `lineStart ..
717
+ * lineEnd`, `HTTP1_NEED_MORE` when no LF has arrived yet, or the refusal. A
718
+ * line ends in CRLF and in nothing else. A line longer than `limit` bytes is
719
+ * refused with `overCode`, and that is decided before the bare-LF check, so
720
+ * that a long line ending in a bare LF is refused with the same code whether
721
+ * its LF arrived with it or after the limit was already passed.
722
+ */
723
+ const http1ClientTakeLine = (p: Http1ResponseParser, limit: i32, overCode: i32, overText: string): i32 => {
724
+ const buf: u8[] = p.buf
725
+ const end: i32 = p.end
726
+ let i: i32 = p.scan > p.start ? p.scan : p.start
727
+ while (i >= 0 && i < end && i < toI32(buf.length)) {
728
+ if (buf[i] === 10) {
729
+ if (i - p.start > limit + 1) {
730
+ return http1ClientFail(p, overCode, overText)
731
+ }
732
+ if (i === p.start || buf[i - 1] !== 13) {
733
+ return http1ClientFail(p, HTTP1_E_BARE_LF, "a line ended in a bare LF")
734
+ }
735
+ p.lineStart = p.start
736
+ p.lineEnd = i - 1
737
+ p.start = i + 1
738
+ p.scan = i + 1
739
+ return HTTP1_CLIENT_LINE
740
+ }
741
+ i += 1
742
+ }
743
+ p.scan = end
744
+ if (end - p.start > limit + 1) {
745
+ return http1ClientFail(p, overCode, overText)
746
+ }
747
+ return HTTP1_NEED_MORE
748
+ }
749
+
750
+ /** Whether `buf[i]` is an ASCII digit. */
751
+ const http1ClientDigitAt = (buf: u8[], i: i32): boolean =>
752
+ i >= 0 && i < toI32(buf.length) && buf[i] >= 48 && buf[i] <= 57
753
+
754
+ /**
755
+ * Reads the status line in `lineStart .. lineEnd` (RFC 9112 §4):
756
+ * `HTTP/` DIGIT `.` DIGIT SP 3DIGIT SP reason-phrase. A missing reason and its
757
+ * space are taken as an empty reason, since the reason means nothing.
758
+ */
759
+ const http1ClientStatusLine = (p: Http1ResponseParser): i32 => {
760
+ const buf: u8[] = p.buf
761
+ const a: i32 = p.lineStart
762
+ const b: i32 = p.lineEnd < toI32(buf.length) ? p.lineEnd : toI32(buf.length)
763
+ if (
764
+ b - a < 12 ||
765
+ a < 0 ||
766
+ buf[a] !== 72 ||
767
+ buf[a + 1] !== 84 ||
768
+ buf[a + 2] !== 84 ||
769
+ buf[a + 3] !== 80 ||
770
+ buf[a + 4] !== 47 ||
771
+ !http1ClientDigitAt(buf, a + 5) ||
772
+ buf[a + 6] !== 46 ||
773
+ !http1ClientDigitAt(buf, a + 7) ||
774
+ buf[a + 8] !== 32 ||
775
+ !http1ClientDigitAt(buf, a + 9) ||
776
+ !http1ClientDigitAt(buf, a + 10) ||
777
+ !http1ClientDigitAt(buf, a + 11) ||
778
+ (b - a > 12 && buf[a + 12] !== 32)
779
+ ) {
780
+ return http1ClientFail(p, HTTP1_E_STATUS_LINE, "a malformed status line")
781
+ }
782
+ if (buf[a + 5] !== 49) {
783
+ return http1ClientFail(p, HTTP1_E_VERSION, "an HTTP major version other than 1")
784
+ }
785
+ const status: i32 =
786
+ (toI32(buf[a + 9]) - 48) * 100 + (toI32(buf[a + 10]) - 48) * 10 + (toI32(buf[a + 11]) - 48)
787
+ if (status < 100 || status > 599) {
788
+ return http1ClientFail(p, HTTP1_E_STATUS_LINE, "a status code outside 100 to 599")
789
+ }
790
+ for (let k: i32 = a + 13; k < b && k < toI32(buf.length); k += 1) {
791
+ if (!http1IsValueByte(toI32(buf[k]))) {
792
+ return http1ClientFail(p, HTTP1_E_STATUS_LINE, "a control character in the reason phrase")
793
+ }
794
+ }
795
+ p.minor = buf[a + 7] === 48 ? 0 : 1
796
+ p.status = status
797
+ p.reason = b - a > 13 ? http1Text(buf, a + 13, b, false) : ""
798
+ p.state = HTTP1_CLIENT_S_FIELDS
799
+ return HTTP1_NEED_MORE
800
+ }
801
+
802
+ /**
803
+ * Reads one field line in `lineStart .. lineEnd` (RFC 9112 §5) into `names`
804
+ * and `values`, or, for a trailer, only checks it. Answers `HTTP1_NEED_MORE`
805
+ * or the refusal.
806
+ */
807
+ const http1ClientFieldLine = (p: Http1ResponseParser, keep: boolean): i32 => {
808
+ const buf: u8[] = p.buf
809
+ const a: i32 = p.lineStart
810
+ const b: i32 = p.lineEnd < toI32(buf.length) ? p.lineEnd : toI32(buf.length)
811
+ if (a >= 0 && a < b && http1IsOws(toI32(buf[a]))) {
812
+ return http1ClientFail(p, HTTP1_E_FIELD, "a field line that starts with whitespace (obs-fold)")
813
+ }
814
+ let i: i32 = a
815
+ while (i >= 0 && i < b && i < toI32(buf.length) && http1IsTokenByte(toI32(buf[i]))) {
816
+ i += 1
817
+ }
818
+ if (i === a || i >= b || buf[i] !== 58) {
819
+ return http1ClientFail(p, HTTP1_E_FIELD, "a malformed field name")
820
+ }
821
+ const nameEnd: i32 = i
822
+ for (let k: i32 = nameEnd + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
823
+ if (!http1IsValueByte(toI32(buf[k]))) {
824
+ return http1ClientFail(p, HTTP1_E_FIELD, "a control character in a field value")
825
+ }
826
+ }
827
+ p.fieldCount += 1
828
+ if (p.fieldCount > p.maxHeaders) {
829
+ return http1ClientFail(p, HTTP1_E_COUNT_LIMIT, "more fields than the limit")
830
+ }
831
+ // A trailer is checked and counted, but not kept: nothing here reads one.
832
+ if (keep) {
833
+ let valueStart: i32 = nameEnd + 1
834
+ let valueEnd: i32 = b
835
+ while (
836
+ valueStart >= 0 &&
837
+ valueStart < valueEnd &&
838
+ valueStart < toI32(buf.length) &&
839
+ http1IsOws(toI32(buf[valueStart]))
840
+ ) {
841
+ valueStart += 1
842
+ }
843
+ while (
844
+ valueEnd > valueStart &&
845
+ valueEnd - 1 < toI32(buf.length) &&
846
+ http1IsOws(toI32(buf[valueEnd - 1]))
847
+ ) {
848
+ valueEnd -= 1
849
+ }
850
+ p.names.push(http1Text(buf, a, nameEnd, true))
851
+ p.values.push(http1Text(buf, valueStart, valueEnd, false))
852
+ }
853
+ return HTTP1_NEED_MORE
854
+ }
855
+
856
+ /**
857
+ * Reads one `Content-Length` value: a comma-separated list (RFC 9110 §8.6
858
+ * lets a recipient take a list of one repeated value as that value) whose
859
+ * elements are each a run of digits and all the same as each other and as
860
+ * `seen`, the value of an earlier field (-1 for none). Answers the value, -1
861
+ * when an element is empty or not digits, or -2 when two values differ. A
862
+ * value past 2^53 is saturated to 2^53 + 1: over any `maxBody`. Saturated
863
+ * values cannot be told apart, so one seen with any other value is -2.
864
+ */
865
+ const http1ClientLengthList = (value: string, seen: i64): i64 => {
866
+ const n: i32 = toI32(value.length)
867
+ let found: i64 = seen
868
+ let start: i32 = 0
869
+ for (let k: i32 = 0; k <= n; k += 1) {
870
+ if (k === n || http1ClientIsComma(value, k)) {
871
+ const a: i32 = http1ClientTrimStart(value, start, k)
872
+ const b: i32 = http1ClientTrimEnd(value, a, k)
873
+ if (a === b) {
874
+ return toI64(-1)
875
+ }
876
+ let v: i64 = toI64(0)
877
+ for (let j: i32 = a; j >= 0 && j < b && j < n; j += 1) {
878
+ const c: i32 = toI32(value.charCodeAt(j))
879
+ if (c < 48 || c > 57) {
880
+ return toI64(-1)
881
+ }
882
+ if (v <= (HTTP1_CLIENT_MAX_BODY - toI64(c - 48)) / toI64(10)) {
883
+ v = v * toI64(10) + toI64(c - 48)
884
+ } else {
885
+ v = HTTP1_CLIENT_MAX_BODY + toI64(1)
886
+ }
887
+ }
888
+ if (found >= toI64(0) && (found !== v || v > HTTP1_CLIENT_MAX_BODY)) {
889
+ return toI64(-2)
890
+ }
891
+ found = v
892
+ start = k + 1
893
+ }
894
+ }
895
+ return found
896
+ }
897
+
898
+ /**
899
+ * The fields of `name` together as one comma-separated list (RFC 9110 §5.3),
900
+ * tallied for `word`: answers the element count times 4, plus 2 when `word`
901
+ * appears more than once and 1 when it is the last element.
902
+ */
903
+ const http1ClientTally = (p: Http1ResponseParser, name: string, word: string): i32 => {
904
+ let elements: i32 = 0
905
+ let matches: i32 = 0
906
+ let last: boolean = false
907
+ const names: string[] = p.names
908
+ const values: string[] = p.values
909
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(values.length); i += 1) {
910
+ if (names[i] === name) {
911
+ const value: string = values[i]
912
+ const n: i32 = toI32(value.length)
913
+ let start: i32 = 0
914
+ for (let k: i32 = 0; k <= n; k += 1) {
915
+ if (k === n || http1ClientIsComma(value, k)) {
916
+ const a: i32 = http1ClientTrimStart(value, start, k)
917
+ const b: i32 = http1ClientTrimEnd(value, a, k)
918
+ if (b > a) {
919
+ elements += 1
920
+ last = http1SameWord(value, a, b, word)
921
+ if (last) {
922
+ matches += 1
923
+ }
924
+ }
925
+ start = k + 1
926
+ }
927
+ }
928
+ }
929
+ }
930
+ return elements * 4 + (matches > 1 ? 2 : 0) + (last ? 1 : 0)
931
+ }
932
+
933
+ /** Whether a `Connection` field of the response lists `word`. */
934
+ const http1ClientConnectionHas = (p: Http1ResponseParser, word: string): boolean => {
935
+ const names: string[] = p.names
936
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
937
+ if (names[i] === "connection" && http1ClientListHas(p.values[i], word)) {
938
+ return true
939
+ }
940
+ }
941
+ return false
942
+ }
943
+
944
+ /**
945
+ * A header section has ended: answer `HTTP1_INFORMATIONAL` for a 1xx, or
946
+ * decide the final response's framing and the connection's fate (RFC 9112
947
+ * §6.3, §9.3) and answer `HTTP1_HEAD`, or the refusal.
948
+ */
949
+ const http1ClientEndOfHead = (p: Http1ResponseParser): i32 => {
950
+ const status: i32 = p.status
951
+ if (status === 101) {
952
+ return http1ClientFail(p, HTTP1_E_SWITCH, "a 101 Switching Protocols this client never asks for")
953
+ }
954
+ if (status < 200) {
955
+ if (p.informational >= HTTP1_MAX_INFORMATIONAL) {
956
+ return http1ClientFail(p, HTTP1_E_INFORMATIONAL_LIMIT, "more interim responses than the limit")
957
+ }
958
+ // The 1xx's status and fields stay readable until the next status line.
959
+ p.informational += 1
960
+ p.state = HTTP1_CLIENT_S_STATUS_LINE
961
+ return HTTP1_INFORMATIONAL
962
+ }
963
+ if (p.wasConnect && status < 300) {
964
+ // RFC 9110 §9.3.6: a 2xx to CONNECT has no content, and its framing
965
+ // fields are ignored; what follows the head is the tunnel's.
966
+ p.state = HTTP1_CLIENT_S_MESSAGE_END
967
+ return HTTP1_HEAD
968
+ }
969
+ let codings: boolean = false
970
+ let length: i64 = toI64(-1)
971
+ const names: string[] = p.names
972
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
973
+ if (names[i] === "transfer-encoding") {
974
+ codings = true
975
+ } else if (names[i] === "content-length") {
976
+ length = http1ClientLengthList(p.values[i], length)
977
+ if (length === toI64(-2)) {
978
+ return http1ClientFail(
979
+ p,
980
+ HTTP1_E_LENGTH,
981
+ "Content-Length values that differ, or one past 2^53 given twice"
982
+ )
983
+ }
984
+ if (length < toI64(0)) {
985
+ return http1ClientFail(p, HTTP1_E_LENGTH, "a Content-Length that is not one run of digits")
986
+ }
987
+ }
988
+ }
989
+ if (codings) {
990
+ if (length >= toI64(0)) {
991
+ return http1ClientFail(p, HTTP1_E_FRAMING, "both Content-Length and Transfer-Encoding")
992
+ }
993
+ if (p.minor === 0) {
994
+ return http1ClientFail(p, HTTP1_E_CODING, "Transfer-Encoding in an HTTP/1.0 response")
995
+ }
996
+ const tally: i32 = http1ClientTally(p, "transfer-encoding", "chunked")
997
+ if ((tally & 1) === 0) {
998
+ return http1ClientFail(p, HTTP1_E_CODING, "a transfer coding list that does not end in chunked")
999
+ }
1000
+ if ((tally & 2) !== 0) {
1001
+ return http1ClientFail(p, HTTP1_E_CODING, "chunked applied twice")
1002
+ }
1003
+ if (tally >> 2 > 1) {
1004
+ return http1ClientFail(p, HTTP1_E_CODING, "a transfer coding other than chunked")
1005
+ }
1006
+ }
1007
+ const close: boolean = http1ClientConnectionHas(p, "close")
1008
+ p.persistent = p.minor === 1 ? !close : !close && http1ClientConnectionHas(p, "keep-alive")
1009
+ if (p.wasHead || status === 204 || status === 304) {
1010
+ // RFC 9112 §6.3 rules 1 and 2: no content, whatever the fields say,
1011
+ // and their syntax has been checked so that a smuggled one still fails.
1012
+ p.state = HTTP1_CLIENT_S_MESSAGE_END
1013
+ return HTTP1_HEAD
1014
+ }
1015
+ if (codings) {
1016
+ p.chunked = true
1017
+ p.state = HTTP1_CLIENT_S_CHUNK_SIZE
1018
+ } else if (length >= toI64(0)) {
1019
+ if (length > p.maxBody) {
1020
+ return http1ClientFail(p, HTTP1_E_BODY_LIMIT, "a Content-Length over the limit")
1021
+ }
1022
+ p.contentLength = length
1023
+ p.left = length
1024
+ p.state = length > toI64(0) ? HTTP1_CLIENT_S_LENGTH_BODY : HTTP1_CLIENT_S_MESSAGE_END
1025
+ } else {
1026
+ p.persistent = false
1027
+ p.state = HTTP1_CLIENT_S_CLOSE_BODY
1028
+ }
1029
+ return HTTP1_HEAD
1030
+ }
1031
+
1032
+ /**
1033
+ * Reads a chunk-size line (RFC 9112 §7.1): 1*HEXDIG, then optional
1034
+ * whitespace and a `;` extension that is read past but not interpreted. Sets
1035
+ * `left` and answers `HTTP1_NEED_MORE`, or the refusal.
1036
+ */
1037
+ const http1ClientChunkSize = (p: Http1ResponseParser): i32 => {
1038
+ const buf: u8[] = p.buf
1039
+ const a: i32 = p.lineStart
1040
+ const b: i32 = p.lineEnd < toI32(buf.length) ? p.lineEnd : toI32(buf.length)
1041
+ const room: i64 = p.maxBody - p.bodyTotal
1042
+ let size: i64 = toI64(0)
1043
+ let over: boolean = false
1044
+ let i: i32 = a
1045
+ while (i >= 0 && i < b && i < toI32(buf.length)) {
1046
+ const digit: i32 = http1HexValue(toI32(buf[i]))
1047
+ if (digit < 0) {
1048
+ break
1049
+ }
1050
+ if (!over) {
1051
+ if (toI64(digit) > room || size > (room - toI64(digit)) / toI64(16)) {
1052
+ over = true
1053
+ } else {
1054
+ size = size * toI64(16) + toI64(digit)
1055
+ }
1056
+ }
1057
+ i += 1
1058
+ }
1059
+ if (i === a) {
1060
+ return http1ClientFail(p, HTTP1_E_CHUNK, "a chunk size with no hex digits")
1061
+ }
1062
+ // Whitespace may follow the size only as the BWS before a chunk extension's
1063
+ // `;`, so whitespace that runs to the end of the line is refused.
1064
+ const digitsEnd: i32 = i
1065
+ while (i >= 0 && i < b && i < toI32(buf.length) && http1IsOws(toI32(buf[i]))) {
1066
+ i += 1
1067
+ }
1068
+ if (i >= b && i > digitsEnd) {
1069
+ return http1ClientFail(p, HTTP1_E_CHUNK, "a malformed chunk size")
1070
+ }
1071
+ if (i >= 0 && i < b) {
1072
+ if (buf[i] !== 59) {
1073
+ return http1ClientFail(p, HTTP1_E_CHUNK, "a malformed chunk size")
1074
+ }
1075
+ for (let k: i32 = i + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
1076
+ if (!http1IsValueByte(toI32(buf[k]))) {
1077
+ return http1ClientFail(p, HTTP1_E_CHUNK, "a control character in a chunk extension")
1078
+ }
1079
+ }
1080
+ }
1081
+ if (over) {
1082
+ return http1ClientFail(p, HTTP1_E_BODY_LIMIT, "a chunked body over the limit")
1083
+ }
1084
+ p.left = size
1085
+ p.state = size === toI64(0) ? HTTP1_CLIENT_S_TRAILERS : HTTP1_CLIENT_S_CHUNK_DATA
1086
+ p.sectionBytes = 0
1087
+ return HTTP1_NEED_MORE
1088
+ }
1089
+
1090
+ /**
1091
+ * Hands the caller up to `p.left` of the buffered body bytes, and moves to
1092
+ * `after` once the last of them is handed out.
1093
+ */
1094
+ const http1ClientBodyWindow = (p: Http1ResponseParser, after: i32): i32 => {
1095
+ const ready: i32 = p.end - p.start
1096
+ if (ready <= 0) {
1097
+ return HTTP1_NEED_MORE
1098
+ }
1099
+ const take: i32 = toI64(ready) < p.left ? ready : toI32(p.left)
1100
+ p.body = p.buf
1101
+ p.bodyOff = p.start
1102
+ p.bodyLen = take
1103
+ p.start += take
1104
+ p.scan = p.start
1105
+ p.left = p.left - toI64(take)
1106
+ p.bodyTotal = p.bodyTotal + toI64(take)
1107
+ if (p.left === toI64(0)) {
1108
+ p.state = after
1109
+ }
1110
+ return HTTP1_BODY
1111
+ }
1112
+
1113
+ /** A body read until the server closes: every byte fed, up to `maxBody`. */
1114
+ const http1ClientCloseBody = (p: Http1ResponseParser): i32 => {
1115
+ const ready: i32 = p.end - p.start
1116
+ if (ready <= 0) {
1117
+ return HTTP1_NEED_MORE
1118
+ }
1119
+ const room: i64 = p.maxBody - p.bodyTotal
1120
+ if (room <= toI64(0)) {
1121
+ return http1ClientFail(p, HTTP1_E_BODY_LIMIT, "a body read until close over the limit")
1122
+ }
1123
+ // `left` is the room, and the state stays: only the close ends this body.
1124
+ p.left = room
1125
+ return http1ClientBodyWindow(p, HTTP1_CLIENT_S_CLOSE_BODY)
1126
+ }
1127
+
1128
+ /** The state machine behind `next()`. */
1129
+ const http1ClientNext = (p: Http1ResponseParser): i32 => {
1130
+ while (true) {
1131
+ switch (p.state) {
1132
+ case HTTP1_CLIENT_S_STATUS_LINE: {
1133
+ const got: i32 = http1ClientTakeLine(
1134
+ p,
1135
+ p.maxStatusLine,
1136
+ HTTP1_E_STATUS_LINE_LIMIT,
1137
+ "a status line over the limit"
1138
+ )
1139
+ if (got !== HTTP1_CLIENT_LINE) {
1140
+ return got
1141
+ }
1142
+ http1ClientStartHead(p)
1143
+ if (http1ClientStatusLine(p) === HTTP1_ERROR) {
1144
+ return HTTP1_ERROR
1145
+ }
1146
+ break
1147
+ }
1148
+ case HTTP1_CLIENT_S_FIELDS:
1149
+ case HTTP1_CLIENT_S_TRAILERS: {
1150
+ const trailer: boolean = p.state === HTTP1_CLIENT_S_TRAILERS
1151
+ const limit: i32 = trailer ? p.maxTrailerBytes : p.maxHeaderBytes
1152
+ const code: i32 = trailer ? HTTP1_E_TRAILER_LIMIT : HTTP1_E_HEADER_LIMIT
1153
+ const text: string = trailer ? "a trailer section over the limit" : "a header section over the limit"
1154
+ const got: i32 = http1ClientTakeLine(p, limit - p.sectionBytes, code, text)
1155
+ if (got !== HTTP1_CLIENT_LINE) {
1156
+ return got
1157
+ }
1158
+ p.sectionBytes += p.lineEnd - p.lineStart + 2
1159
+ if (p.sectionBytes > limit) {
1160
+ return http1ClientFail(p, code, text)
1161
+ }
1162
+ if (p.lineEnd === p.lineStart) {
1163
+ if (trailer) {
1164
+ p.state = HTTP1_CLIENT_S_MESSAGE_END
1165
+ break
1166
+ }
1167
+ return http1ClientEndOfHead(p)
1168
+ }
1169
+ if (http1ClientFieldLine(p, !trailer) === HTTP1_ERROR) {
1170
+ return HTTP1_ERROR
1171
+ }
1172
+ break
1173
+ }
1174
+ case HTTP1_CLIENT_S_LENGTH_BODY:
1175
+ return http1ClientBodyWindow(p, HTTP1_CLIENT_S_MESSAGE_END)
1176
+ case HTTP1_CLIENT_S_CLOSE_BODY:
1177
+ return http1ClientCloseBody(p)
1178
+ case HTTP1_CLIENT_S_CHUNK_SIZE: {
1179
+ const got: i32 = http1ClientTakeLine(
1180
+ p,
1181
+ HTTP1_MAX_CHUNK_LINE,
1182
+ HTTP1_E_CHUNK_LINE_LIMIT,
1183
+ "a chunk-size line over the limit"
1184
+ )
1185
+ if (got !== HTTP1_CLIENT_LINE) {
1186
+ return got
1187
+ }
1188
+ if (http1ClientChunkSize(p) === HTTP1_ERROR) {
1189
+ return HTTP1_ERROR
1190
+ }
1191
+ break
1192
+ }
1193
+ case HTTP1_CLIENT_S_CHUNK_DATA:
1194
+ return http1ClientBodyWindow(p, HTTP1_CLIENT_S_CHUNK_CRLF)
1195
+ case HTTP1_CLIENT_S_CHUNK_CRLF: {
1196
+ // The CRLF after chunk data is an empty line, so a limit of zero
1197
+ // refuses anything else in its place.
1198
+ const got: i32 = http1ClientTakeLine(p, 0, HTTP1_E_CHUNK, "chunk data not followed by CRLF")
1199
+ if (got !== HTTP1_CLIENT_LINE) {
1200
+ return got
1201
+ }
1202
+ p.state = HTTP1_CLIENT_S_CHUNK_SIZE
1203
+ break
1204
+ }
1205
+ case HTTP1_CLIENT_S_MESSAGE_END: {
1206
+ if (p.wasConnect && p.status < 300) {
1207
+ p.state = HTTP1_CLIENT_S_TUNNEL
1208
+ } else if (!p.persistent) {
1209
+ p.state = HTTP1_CLIENT_S_CLOSED
1210
+ } else {
1211
+ // The fields stay readable until the next status line arrives.
1212
+ p.state = HTTP1_CLIENT_S_STATUS_LINE
1213
+ p.informational = 0
1214
+ p.wasHead = false
1215
+ p.wasConnect = false
1216
+ }
1217
+ return HTTP1_END
1218
+ }
1219
+ case HTTP1_CLIENT_S_TUNNEL:
1220
+ return HTTP1_UPGRADE
1221
+ case HTTP1_CLIENT_S_CLOSED:
1222
+ return HTTP1_CLOSED
1223
+ default:
1224
+ return HTTP1_ERROR
1225
+ }
1226
+ }
1227
+ }