@amritk/nish-x86_64-linux 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/INSTALL.md +43 -34
  2. package/bin/nish +0 -0
  3. package/package.json +1 -1
  4. package/runtime/nish.d.ts +117 -8
  5. package/runtime/nish.h +50 -13
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +43 -24
  8. package/runtime/runtime-net.c +57 -10
  9. package/runtime/runtime-os.c +62 -20
  10. package/runtime/runtime-simd.c +203 -0
  11. package/runtime/runtime-wasm.c +12 -0
  12. package/runtime/runtime.c +66 -18
  13. package/runtime/shim.mjs +114 -5
  14. package/scripts/build.sh +14 -12
  15. package/std/README.md +168 -36
  16. package/std/crypto/aes.ts +51 -8
  17. package/std/crypto/base64url.ts +25 -12
  18. package/std/crypto/chacha20poly1305.ts +43 -29
  19. package/std/crypto/ct.ts +6 -1
  20. package/std/crypto/hkdf.ts +364 -3
  21. package/std/crypto/hmac.ts +307 -7
  22. package/std/crypto/p256.ts +618 -34
  23. package/std/crypto/sha1.ts +147 -0
  24. package/std/crypto/sha256.ts +3 -2
  25. package/std/crypto/x25519.ts +88 -36
  26. package/std/crypto/x509.ts +130 -47
  27. package/std/json.ts +297 -41
  28. package/std/net/hpack.ts +1094 -0
  29. package/std/net/http-fields.ts +604 -0
  30. package/std/net/http1-server.ts +1389 -0
  31. package/std/net/http1.ts +1492 -0
  32. package/std/net/http2-frame.ts +745 -0
  33. package/std/net/http2-tls.ts +216 -0
  34. package/std/net/http2.ts +1433 -0
  35. package/std/net/http3-frame.ts +457 -0
  36. package/std/net/http3-server.ts +911 -0
  37. package/std/net/http3.ts +2145 -0
  38. package/std/net/qpack.ts +1163 -0
  39. package/std/net/quic-conn-ack.ts +206 -0
  40. package/std/net/quic-conn-cid.ts +425 -0
  41. package/std/net/quic-conn-params.ts +462 -0
  42. package/std/net/quic-datagram.ts +130 -0
  43. package/std/net/quic-frame.ts +1206 -0
  44. package/std/net/quic-listener.ts +929 -0
  45. package/std/net/quic-packet.ts +1698 -0
  46. package/std/net/quic-recovery.ts +849 -0
  47. package/std/net/quic-stream.ts +1609 -0
  48. package/std/net/quic.ts +3578 -0
  49. package/std/net/tls/codec.ts +1089 -0
  50. package/std/net/tls/record-server.ts +1173 -0
  51. package/std/net/tls/record.ts +621 -0
  52. package/std/net/tls/schedule.ts +429 -0
  53. package/std/net/tls-tcp.ts +414 -0
  54. package/std/net/tls.ts +1437 -0
  55. package/std/net/websocket.ts +823 -0
  56. package/std/net/webtransport.ts +1334 -0
  57. package/std/secret.ts +62 -0
  58. package/std/text.ts +77 -0
@@ -0,0 +1,1492 @@
1
+ /**
2
+ * `nish/net/http1` — an incremental HTTP/1.1 request parser and a response
3
+ * writer, sans-IO: bytes in, events and bytes out, and no socket anywhere.
4
+ *
5
+ * **The parser** is a push-pull machine. A caller `feed`s whatever slice the
6
+ * socket gave it, of any length and cut anywhere, and then calls `next()`
7
+ * until it answers `HTTP1_NEED_MORE`:
8
+ *
9
+ * import { Http1Parser, HTTP1_HEAD, HTTP1_BODY, HTTP1_END, HTTP1_ERROR } from "nish/net/http1";
10
+ *
11
+ * const p = new Http1Parser(maxTarget, maxHeaderBytes, maxHeaders, maxBody);
12
+ * p.feed(received, 0, n);
13
+ * let event: i32 = p.next();
14
+ * while (event !== HTTP1_NEED_MORE) {
15
+ * // HTTP1_HEAD: p.method, p.target, p.header("host"), ...
16
+ * // HTTP1_BODY: p.body[p.bodyOff .. p.bodyOff + p.bodyLen)
17
+ * // HTTP1_ERROR: answer p.status and close
18
+ * event = p.next();
19
+ * }
20
+ *
21
+ * The events are `HTTP1_HEAD` (the request line and header section are in),
22
+ * `HTTP1_BODY` (some body bytes, de-chunked), `HTTP1_END` (the message is
23
+ * complete; with keep-alive the next request follows), `HTTP1_UPGRADE` (the
24
+ * request asked to switch protocols and has ended: the bytes after it are not
25
+ * HTTP), `HTTP1_CLOSED` (the request said `Connection: close`, so nothing
26
+ * after it is read) and `HTTP1_ERROR` (with the status to answer in `status`).
27
+ * The last three repeat on every later call. Only complete lines are acted on
28
+ * and body bytes are counted, never guessed, so the sequence of heads, the
29
+ * concatenated body and the final event do not depend on where the input was
30
+ * cut — `tests/link/net_http1_split` feeds a corpus at every split point.
31
+ *
32
+ * **What it refuses, and with what** (RFC 9112 unless named):
33
+ *
34
+ * - 400: a malformed request line or field line; a bare LF or a stray CR
35
+ * (§2.2); whitespace before the first field or an obs-fold (§2.2, §5.2);
36
+ * whitespace between a field name and its colon (§5.1); a field value with a
37
+ * control character; a 1.1 request without exactly one `Host`, or a 1.0
38
+ * request with more than one (§3.2, RFC 9110 §7.2); `Content-Length` that is
39
+ * not one run of digits, or that appears twice (§6.3); `Transfer-Encoding`
40
+ * together with `Content-Length`, in a 1.0 request, or whose last coding is
41
+ * not `chunked` (§6.1, §6.3); a malformed chunk size, chunk extension or
42
+ * chunk terminator (§7.1). These are the request-smuggling shapes, and each
43
+ * one is refused rather than repaired, because two parsers that repair one
44
+ * differently disagree on where the next request starts.
45
+ * - 413: a declared or a de-chunked body longer than `maxBody`.
46
+ * - 414: a request target longer than `maxTarget`.
47
+ * - 431: a header (or trailer) section longer than `maxHeaderBytes`, or with
48
+ * more than `maxHeaders` fields.
49
+ * - 501: a transfer coding other than `chunked` (§6.1).
50
+ * - 505: an HTTP major version other than 1. A 1.x minor version above 1 is
51
+ * read as 1.1, as RFC 9110 §6.2 asks.
52
+ *
53
+ * An error is final: the parser answers `HTTP1_ERROR` from then on and
54
+ * ignores further input, and the caller answers `status` and closes. Every
55
+ * limit is the caller's number. A method is at most `HTTP1_MAX_METHOD` bytes.
56
+ *
57
+ * **Connection management.** `keepAlive` is true for a 1.1 request unless
58
+ * `Connection` lists `close`, and for a 1.0 request only when it lists
59
+ * `keep-alive` (§9.3). `upgrade` is the `Upgrade` field's value when a 1.1
60
+ * request also lists `upgrade` in `Connection`, and empty otherwise (RFC 9110
61
+ * §7.8 says a 1.0 request's `Upgrade` is ignored). After such a request's
62
+ * `HTTP1_END` the parser stops at `HTTP1_UPGRADE`; `upgradeBytes()` answers
63
+ * what was fed past the request, for the next protocol, and `declineUpgrade()`
64
+ * goes back to HTTP for a server that answered without switching.
65
+ *
66
+ * **The writer** is `http1ResponseHead`, which answers a status line and
67
+ * header section with the framing field the body needs, and `http1Chunk` and
68
+ * `http1LastChunk` for a chunked body. It refuses (`null`) anything that would
69
+ * let a caller's string end a line early — the response-splitting shape.
70
+ *
71
+ * Field names are kept in lowercase, the form RFC 9110 §5.1 says they compare
72
+ * in and the form HTTP/2 sends them in. Strings in the language are bytes, so a
73
+ * field value holding obs-text (0x80 to 0xFF) arrives byte for byte.
74
+ *
75
+ * Private helpers share the importing program's flat symbol namespace
76
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
77
+ *
78
+ * Written from RFC 9112 and RFC 9110, not ported from another implementation.
79
+ */
80
+
81
+ /** `next()`: the input so far holds no further event; feed more. */
82
+ export const HTTP1_NEED_MORE: i32 = 0
83
+ /** `next()`: a request line and header section were read. */
84
+ export const HTTP1_HEAD: i32 = 1
85
+ /** `next()`: `bodyLen` body bytes are at `body[bodyOff ..]`, valid until the next `feed`. */
86
+ export const HTTP1_BODY: i32 = 2
87
+ /** `next()`: the request's body is complete. */
88
+ export const HTTP1_END: i32 = 3
89
+ /** `next()`, from now on: the request switched protocols; see `upgradeBytes()`. */
90
+ export const HTTP1_UPGRADE: i32 = 4
91
+ /** `next()`, from now on: the request closed the connection; nothing more is read. */
92
+ export const HTTP1_CLOSED: i32 = 5
93
+ /** `next()`, from now on: the input is refused; `status` and `reason` say why. */
94
+ export const HTTP1_ERROR: i32 = 6
95
+
96
+ /** The longest method this parser reads; a longer one is a 400. */
97
+ export const HTTP1_MAX_METHOD: i32 = 32
98
+
99
+ /** `http1ResponseHead`'s `bodyLength` for a chunked body. */
100
+ export const HTTP1_CHUNKED: i32 = -1
101
+ /** `http1ResponseHead`'s `bodyLength` for a response that carries no body field (1xx, 204, 304, HEAD). */
102
+ export const HTTP1_NO_BODY: i32 = -2
103
+
104
+ /** `limit` clamped into 0 to 2^30. */
105
+ const http1Clamp = (limit: i32): i32 => {
106
+ if (limit < 0) {
107
+ return 0
108
+ }
109
+ return limit > 1073741824 ? 1073741824 : limit
110
+ }
111
+
112
+ // The parser's states, one per kind of thing it is waiting for.
113
+ const HTTP1_S_REQUEST_LINE: i32 = 0
114
+ const HTTP1_S_FIELDS: i32 = 1
115
+ const HTTP1_S_LENGTH_BODY: i32 = 2
116
+ const HTTP1_S_CHUNK_SIZE: i32 = 3
117
+ const HTTP1_S_CHUNK_DATA: i32 = 4
118
+ const HTTP1_S_CHUNK_CRLF: i32 = 5
119
+ const HTTP1_S_TRAILERS: i32 = 6
120
+ const HTTP1_S_MESSAGE_END: i32 = 7
121
+ const HTTP1_S_UPGRADED: i32 = 8
122
+ const HTTP1_S_CLOSED: i32 = 9
123
+ const HTTP1_S_ERROR: i32 = 10
124
+
125
+ /**
126
+ * What `http1Tally` found in a comma-separated list: how many elements it
127
+ * has, how many of them are the word asked about, and whether the last is.
128
+ */
129
+ class Http1ListTally {
130
+ elements: i32 = 0
131
+ matches: i32 = 0
132
+ lastMatches: boolean = false
133
+ }
134
+
135
+ /**
136
+ * One connection's request parser, from the first byte to the close: requests
137
+ * on a kept-alive connection follow one another through the same parser.
138
+ */
139
+ export class Http1Parser {
140
+ // The fields are declared widest first, references before numbers before
141
+ // flags, so the struct carries no padding; the groups say what each is.
142
+
143
+ // --- the request: valid from its `HTTP1_HEAD` until the next request's -----
144
+ method: string = ""
145
+ target: string = ""
146
+ /** The field names, in lowercase, in the order they arrived; `values` beside them. */
147
+ names: string[]
148
+ values: string[]
149
+ /** The `Upgrade` field when `Connection` asked for it, else empty. */
150
+ upgrade: string = ""
151
+ /** The buffer the last `HTTP1_BODY`'s bytes are in, at `bodyOff`, `bodyLen` long. */
152
+ body: u8[]
153
+ /** What was wrong, once `next()` said `HTTP1_ERROR`; for a log, not for the response. */
154
+ reason: string = ""
155
+ /** The bytes fed and not yet consumed are `buf[start .. end)`. */
156
+ buf: u8[]
157
+ /**
158
+ * The request line and the kept field lines of the request, copied out of
159
+ * `buf` as each is read, names in lowercase: what the spans below index.
160
+ * Kept from one request to the next, so a slot that has seen its largest
161
+ * head allocates nothing for the next one.
162
+ */
163
+ head: u8[]
164
+ /** Four entries per field, into `head`: name start and end, value start and end (OWS trimmed). */
165
+ spans: i32[]
166
+ /** Reused by every list `http1Tally` walks. */
167
+ tally: Http1ListTally
168
+
169
+ // --- limits, the caller's --------------------------------------------------
170
+ maxTarget: i32 = 0
171
+ maxHeaderBytes: i32 = 0
172
+ maxHeaders: i32 = 0
173
+ maxBody: i32 = 0
174
+ /** The most body bytes one `HTTP1_BODY` hands over; a caller's number, 2^30 unless set. */
175
+ maxChunk: i32 = 1073741824
176
+
177
+ /** The minor version: 0 for HTTP/1.0, 1 for HTTP/1.1 and anything above it. */
178
+ minor: i32 = 1
179
+ /** The declared body length, or -1 when the request has none or is chunked. */
180
+ contentLength: i32 = -1
181
+ bodyOff: i32 = 0
182
+ bodyLen: i32 = 0
183
+ /** The status code to answer once `next()` said `HTTP1_ERROR`. */
184
+ status: i32 = 0
185
+ /** The method is `head[methodStart .. methodEnd)` and the target `head[targetStart .. targetEnd)`. */
186
+ methodStart: i32 = 0
187
+ methodEnd: i32 = 0
188
+ targetStart: i32 = 0
189
+ targetEnd: i32 = 0
190
+ /** How many fields the request's header section kept: the spans of field `i` are `spans[4 * i ..]`. */
191
+ headerCount: i32 = 0
192
+ /** How much of `head` the request uses. */
193
+ headLength: i32 = 0
194
+
195
+ // --- internal --------------------------------------------------------------
196
+ start: i32 = 0
197
+ end: i32 = 0
198
+ /** Where the search for the next LF resumes, so a line fed a byte at a time is scanned once. */
199
+ scan: i32 = 0
200
+ state: i32 = 0
201
+ /** Bytes left in a length-delimited body or in the current chunk. */
202
+ left: i32 = 0
203
+ /** De-chunked body bytes so far, against `maxBody`. */
204
+ bodyTotal: i32 = 0
205
+ /** Bytes of the header or trailer section so far, against `maxHeaderBytes`. */
206
+ sectionBytes: i32 = 0
207
+ /** Field lines of the request so far, its trailers included, against `maxHeaders`. */
208
+ fieldCount: i32 = 0
209
+ /** The line `http1TakeLine` found is `buf[lineStart .. lineEnd)`, its CRLF dropped. */
210
+ lineStart: i32 = 0
211
+ lineEnd: i32 = 0
212
+
213
+ chunked: boolean = false
214
+ keepAlive: boolean = false
215
+ /** Whether the request asked to switch protocols (`upgrade` is its value when `keepText`). */
216
+ upgrading: boolean = false
217
+ /**
218
+ * Whether `method`, `target`, `names`, `values` and `upgrade` are filled as
219
+ * strings. A server that must allocate nothing per request turns it off and
220
+ * reads the head through `head`, the spans and the `...Is` methods.
221
+ */
222
+ keepText: boolean = true
223
+
224
+ /**
225
+ * Each limit is clamped into 0 to 2^30, so that the sums the parser makes
226
+ * of them (a request line is the method, the target and ten bytes more)
227
+ * cannot overflow an `i32`.
228
+ */
229
+ constructor(maxTarget: i32, maxHeaderBytes: i32, maxHeaders: i32, maxBody: i32) {
230
+ this.maxTarget = http1Clamp(maxTarget)
231
+ this.maxHeaderBytes = http1Clamp(maxHeaderBytes)
232
+ this.maxHeaders = http1Clamp(maxHeaders)
233
+ this.maxBody = http1Clamp(maxBody)
234
+ this.buf = new Array<u8>(1024)
235
+ this.body = this.buf
236
+ this.names = []
237
+ this.values = []
238
+ this.head = new Array<u8>(256)
239
+ this.spans = new Array<i32>(64)
240
+ this.tally = new Http1ListTally()
241
+ this.restart()
242
+ }
243
+
244
+ /**
245
+ * Appends `data[off .. off + len)` to the input. A window outside `data`
246
+ * panics, as the crypto modules' `update` does. Once the parser has closed
247
+ * or refused, the input is dropped; once it has upgraded, it is kept for
248
+ * `upgradeBytes()`.
249
+ */
250
+ feed(data: u8[], off: i32, len: i32): void {
251
+ const size: i32 = toI32(data.length)
252
+ if (off < 0 || len < 0 || off > size || len > size - off) {
253
+ panic("Http1Parser: the window is outside the buffer")
254
+ }
255
+ if (this.state === HTTP1_S_CLOSED || this.state === HTTP1_S_ERROR) {
256
+ return
257
+ }
258
+ http1Reserve(this, len)
259
+ const buf: u8[] = this.buf
260
+ const at: i32 = this.end
261
+ // A whole array is one `set`, a memmove; a window is copied byte by byte,
262
+ // because `set` takes no window of its source.
263
+ if (off === 0 && len === size) {
264
+ buf.set(data, at)
265
+ this.end = at + len
266
+ return
267
+ }
268
+ for (let k: i32 = 0; k < len; k += 1) {
269
+ const to: i32 = at + k
270
+ if (to >= 0 && to < toI32(buf.length)) {
271
+ buf[to] = data[off + k]
272
+ }
273
+ }
274
+ this.end = at + len
275
+ }
276
+
277
+ /** The next event the input holds; see the module comment. */
278
+ next(): i32 {
279
+ return http1Next(this)
280
+ }
281
+
282
+ /**
283
+ * The value of the first field named `name` (any case), or `null`. A field
284
+ * that may repeat is read through the spans. The answer is a fresh string;
285
+ * `headerIndex` and `headerIs` allocate nothing.
286
+ */
287
+ header(name: string): string | null {
288
+ const i: i32 = this.headerIndex(name)
289
+ if (i < 0) {
290
+ return null
291
+ }
292
+ if (this.keepText && i < toI32(this.values.length)) {
293
+ return this.values[i]
294
+ }
295
+ const spans: i32[] = this.spans
296
+ const at: i32 = 4 * i
297
+ if (at + 3 >= toI32(spans.length)) {
298
+ return null
299
+ }
300
+ return http1Text(this.head, spans[at + 2], spans[at + 3], false)
301
+ }
302
+
303
+ /** The index of the first field named `name` (any case), or -1. */
304
+ headerIndex(name: string): i32 {
305
+ const spans: i32[] = this.spans
306
+ const count: i32 = this.headerCount
307
+ for (let i: i32 = 0; i < count && 4 * i + 1 < toI32(spans.length); i += 1) {
308
+ if (http1HeadIs(this.head, spans[4 * i], spans[4 * i + 1], name, true)) {
309
+ return i
310
+ }
311
+ }
312
+ return -1
313
+ }
314
+
315
+ /** Whether the first field named `name` (any case) has exactly the value `value`. */
316
+ headerIs(name: string, value: string): boolean {
317
+ const i: i32 = this.headerIndex(name)
318
+ const spans: i32[] = this.spans
319
+ if (i < 0 || 4 * i + 3 >= toI32(spans.length)) {
320
+ return false
321
+ }
322
+ return http1HeadIs(this.head, spans[4 * i + 2], spans[4 * i + 3], value, false)
323
+ }
324
+
325
+ /** Whether the method is exactly `text` (methods are case-sensitive, RFC 9110 §9.1). */
326
+ methodIs(text: string): boolean {
327
+ return http1HeadIs(this.head, this.methodStart, this.methodEnd, text, false)
328
+ }
329
+
330
+ /** Whether the request target is exactly `text`. */
331
+ targetIs(text: string): boolean {
332
+ return http1HeadIs(this.head, this.targetStart, this.targetEnd, text, false)
333
+ }
334
+
335
+ /**
336
+ * Whether the request asked to switch to exactly `protocol` (any case): one
337
+ * `Upgrade` field holding that one token, with `upgrade` in `Connection`.
338
+ */
339
+ upgradeIs(protocol: string): boolean {
340
+ if (!this.upgrading) {
341
+ return false
342
+ }
343
+ const spans: i32[] = this.spans
344
+ let found: i32 = -1
345
+ for (let i: i32 = 0; i < this.headerCount && 4 * i + 1 < toI32(spans.length); i += 1) {
346
+ if (http1HeadIs(this.head, spans[4 * i], spans[4 * i + 1], "upgrade", false)) {
347
+ if (found >= 0) {
348
+ return false
349
+ }
350
+ found = i
351
+ }
352
+ }
353
+ if (found < 0 || 4 * found + 3 >= toI32(spans.length)) {
354
+ return false
355
+ }
356
+ return http1HeadIs(this.head, spans[4 * found + 2], spans[4 * found + 3], protocol, true)
357
+ }
358
+
359
+ /** How many bytes were fed and not consumed yet: what a caller bounds its reads by. */
360
+ buffered(): i32 {
361
+ return this.end - this.start
362
+ }
363
+
364
+ /** After `HTTP1_UPGRADE`: every byte fed past the request, as a fresh array. */
365
+ upgradeBytes(): u8[] {
366
+ let n: i32 = this.end - this.start
367
+ if (n < 0) {
368
+ n = 0
369
+ }
370
+ const out: u8[] = new Array<u8>(n)
371
+ const outLength: i32 = toI32(out.length)
372
+ const buf: u8[] = this.buf
373
+ const from: i32 = this.start
374
+ for (let k: i32 = 0; k < outLength; k += 1) {
375
+ out[k] = buf[from + k]
376
+ }
377
+ return out
378
+ }
379
+
380
+ /**
381
+ * Puts the parser back as the constructor left it, for the next
382
+ * connection, keeping every buffer it has grown: a slot that is reused
383
+ * allocates nothing to start over.
384
+ */
385
+ restart(): void {
386
+ this.start = 0
387
+ this.end = 0
388
+ this.scan = 0
389
+ this.lineStart = 0
390
+ this.lineEnd = 0
391
+ this.bodyOff = 0
392
+ this.left = 0
393
+ this.status = 0
394
+ this.reason = ""
395
+ this.body = this.buf
396
+ http1StartRequest(this)
397
+ }
398
+
399
+ /**
400
+ * After `HTTP1_UPGRADE`: the server answered without switching, so the
401
+ * bytes that follow are the next HTTP request. Anywhere else it does nothing.
402
+ */
403
+ declineUpgrade(): void {
404
+ if (this.state === HTTP1_S_UPGRADED) {
405
+ http1StartRequest(this)
406
+ }
407
+ }
408
+ }
409
+
410
+ /** Makes room for `len` more bytes at `p.end`, moving the live bytes down first. */
411
+ const http1Reserve = (p: Http1Parser, len: i32): void => {
412
+ const old: u8[] = p.buf
413
+ const capacity: i32 = toI32(old.length)
414
+ if (len <= capacity - p.end) {
415
+ return
416
+ }
417
+ const live: i32 = p.end - p.start
418
+ let size: i32 = capacity
419
+ while (size - live < len) {
420
+ if (size > 1073741823) {
421
+ panic("Http1Parser: more than 2^31 - 1 bytes buffered")
422
+ }
423
+ size = size * 2
424
+ }
425
+ // A fresh array when it has to grow, and the same one otherwise: either way
426
+ // the live bytes move to the front, and every index into them moves too.
427
+ const buf: u8[] = size === capacity ? old : new Array<u8>(size)
428
+ const from: i32 = p.start
429
+ for (let k: i32 = 0; k < live && k < toI32(buf.length); k += 1) {
430
+ buf[k] = old[from + k]
431
+ }
432
+ p.buf = buf
433
+ p.lineStart -= from
434
+ p.lineEnd -= from
435
+ p.scan -= from
436
+ p.start = 0
437
+ p.end = live
438
+ }
439
+
440
+ /** Refuses the input with `status`. Answers `HTTP1_ERROR`, so a caller can `return` it. */
441
+ const http1Fail = (p: Http1Parser, status: i32, reason: string): i32 => {
442
+ p.state = HTTP1_S_ERROR
443
+ p.status = status
444
+ p.reason = reason
445
+ p.keepAlive = false
446
+ return HTTP1_ERROR
447
+ }
448
+
449
+ /** Clears the per-request fields and waits for a request line. */
450
+ const http1StartRequest = (p: Http1Parser): void => {
451
+ p.state = HTTP1_S_REQUEST_LINE
452
+ p.method = ""
453
+ p.target = ""
454
+ p.minor = 1
455
+ if (p.keepText) {
456
+ p.names = []
457
+ p.values = []
458
+ }
459
+ p.headLength = 0
460
+ p.headerCount = 0
461
+ p.methodStart = 0
462
+ p.methodEnd = 0
463
+ p.targetStart = 0
464
+ p.targetEnd = 0
465
+ p.contentLength = -1
466
+ p.chunked = false
467
+ p.keepAlive = false
468
+ p.upgrading = false
469
+ p.upgrade = ""
470
+ p.bodyLen = 0
471
+ p.bodyTotal = 0
472
+ p.sectionBytes = 0
473
+ p.fieldCount = 0
474
+ }
475
+
476
+ /** `http1TakeLine`'s answer when a whole line is ready; never an event. */
477
+ const HTTP1_LINE: i32 = -1
478
+
479
+ /**
480
+ * Finds the next line: answers `HTTP1_LINE` with it in `lineStart ..
481
+ * lineEnd`, `HTTP1_NEED_MORE` when no LF has arrived yet, or the refusal. A line ends in CRLF and in
482
+ * nothing else; a LF without its CR is a 400. A line that has not ended by
483
+ * `limit` bytes is refused with `overStatus`.
484
+ */
485
+ const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
486
+ const buf: u8[] = p.buf
487
+ const end: i32 = p.end
488
+ let i: i32 = p.scan > p.start ? p.scan : p.start
489
+ while (i >= 0 && i < end && i < toI32(buf.length)) {
490
+ if (buf[i] === 10) {
491
+ if (i === p.start || buf[i - 1] !== 13) {
492
+ return http1Fail(p, 400, "a line ended in a bare LF")
493
+ }
494
+ if (i - 1 - p.start > limit) {
495
+ return http1Fail(p, overStatus, "a line over its limit")
496
+ }
497
+ p.lineStart = p.start
498
+ p.lineEnd = i - 1
499
+ p.start = i + 1
500
+ p.scan = i + 1
501
+ return HTTP1_LINE
502
+ }
503
+ i += 1
504
+ }
505
+ p.scan = end
506
+ if (end - p.start > limit + 1) {
507
+ return http1Fail(p, overStatus, "a line over its limit")
508
+ }
509
+ return HTTP1_NEED_MORE
510
+ }
511
+
512
+ /** Whether `c` is a tchar of RFC 9110 §5.6.2, the bytes a token is made of. */
513
+ const http1IsTokenByte = (c: i32): boolean =>
514
+ (c >= 97 && c <= 122) ||
515
+ (c >= 65 && c <= 90) ||
516
+ (c >= 48 && c <= 57) ||
517
+ c === 33 ||
518
+ (c >= 35 && c <= 39) ||
519
+ c === 42 ||
520
+ c === 43 ||
521
+ c === 45 ||
522
+ c === 46 ||
523
+ c === 94 ||
524
+ c === 95 ||
525
+ c === 96 ||
526
+ c === 124 ||
527
+ c === 126
528
+
529
+ /** Whether `c` may appear in a field value (RFC 9110 §5.5): HTAB, SP, VCHAR or obs-text. */
530
+ const http1IsValueByte = (c: i32): boolean => c === 9 || (c >= 32 && c !== 127)
531
+
532
+ /** `buf[from .. to)` as a string, each upper-case ASCII letter lowered when `lower`. */
533
+ const http1Text = (buf: u8[], from: i32, to: i32, lower: boolean): string => {
534
+ const parts: string[] = []
535
+ for (let k: i32 = from; k >= 0 && k < to && k < toI32(buf.length); k += 1) {
536
+ let c: i32 = toI32(buf[k])
537
+ if (lower && c >= 65 && c <= 90) {
538
+ c += 32
539
+ }
540
+ parts.push(String.fromCharCode(c))
541
+ }
542
+ return parts.join("")
543
+ }
544
+
545
+ /** Whether `c` is optional whitespace, a space or a tab (OWS, RFC 9110 §5.6.3). */
546
+ const http1IsOws = (c: i32): boolean => c === 32 || c === 9
547
+
548
+ /** Whether `text[a .. b)` is `word`, which is lowercase, in any ASCII case. */
549
+ const http1SameWord = (text: string, a: i32, b: i32, word: string): boolean => {
550
+ const n: i32 = toI32(word.length)
551
+ if (a < 0 || b - a !== n || b > toI32(text.length)) {
552
+ return false
553
+ }
554
+ for (let k: i32 = 0; k < n; k += 1) {
555
+ let c: i32 = toI32(text.charCodeAt(a + k))
556
+ if (c >= 65 && c <= 90) {
557
+ c += 32
558
+ }
559
+ if (c !== toI32(word.charCodeAt(k))) {
560
+ return false
561
+ }
562
+ }
563
+ return true
564
+ }
565
+
566
+ /**
567
+ * Whether `bytes[a .. b)` is `word`; with `fold`, an uppercase ASCII letter in
568
+ * `word` matches its lowercase form in `bytes`, which is how a stored field
569
+ * name (already in lowercase) is compared with a caller's.
570
+ */
571
+ const http1HeadIs = (bytes: u8[], a: i32, b: i32, word: string, fold: boolean): boolean => {
572
+ const n: i32 = toI32(word.length)
573
+ if (a < 0 || b - a !== n || b > toI32(bytes.length)) {
574
+ return false
575
+ }
576
+ for (let k: i32 = 0; k < n && a + k < toI32(bytes.length); k += 1) {
577
+ let c: i32 = toI32(word.charCodeAt(k))
578
+ let d: i32 = toI32(bytes[a + k])
579
+ if (fold && c >= 65 && c <= 90) {
580
+ c += 32
581
+ }
582
+ if (fold && d >= 65 && d <= 90) {
583
+ d += 32
584
+ }
585
+ if (c !== d) {
586
+ return false
587
+ }
588
+ }
589
+ return true
590
+ }
591
+
592
+ /**
593
+ * Copies `buf[from .. to)` to the end of `head`, growing it when it must, and
594
+ * answers where the copy starts.
595
+ */
596
+ const http1HeadAppend = (p: Http1Parser, from: i32, to: i32): i32 => {
597
+ const n: i32 = to - from
598
+ const at: i32 = p.headLength
599
+ let size: i32 = toI32(p.head.length)
600
+ if (n > size - at) {
601
+ while (size - at < n) {
602
+ if (size > 1073741823) {
603
+ panic("Http1Parser: a head of more than 2^31 - 1 bytes")
604
+ }
605
+ size = size * 2
606
+ }
607
+ const grown: u8[] = new Array<u8>(size)
608
+ const old: u8[] = p.head
609
+ for (let k: i32 = 0; k < at && k < toI32(grown.length) && k < toI32(old.length); k += 1) {
610
+ grown[k] = old[k]
611
+ }
612
+ p.head = grown
613
+ }
614
+ const head: u8[] = p.head
615
+ const buf: u8[] = p.buf
616
+ for (let k: i32 = 0; k < n && at + k < toI32(head.length) && from + k < toI32(buf.length); k += 1) {
617
+ if (from + k >= 0) {
618
+ head[at + k] = buf[from + k]
619
+ }
620
+ }
621
+ p.headLength = at + n
622
+ return at
623
+ }
624
+
625
+ /** Sets `spans[at]`, growing the array when it must. */
626
+ const http1SpanSet = (p: Http1Parser, at: i32, value: i32): void => {
627
+ if (at >= toI32(p.spans.length)) {
628
+ const old: i32[] = p.spans
629
+ const grown: i32[] = new Array<i32>(2 * at + 4)
630
+ for (let k: i32 = 0; k < toI32(old.length) && k < toI32(grown.length); k += 1) {
631
+ grown[k] = old[k]
632
+ }
633
+ p.spans = grown
634
+ }
635
+ const spans: i32[] = p.spans
636
+ if (at >= 0 && at < toI32(spans.length)) {
637
+ spans[at] = value
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Walks the comma-separated list in `head[from .. to)` (RFC 9110 §5.6.1),
643
+ * each element trimmed of OWS and compared with `word` in any case, adding to
644
+ * `tally`. Empty elements are skipped, as the rule says a recipient must
645
+ * accept them. Nothing is copied.
646
+ */
647
+ const http1TallyRun = (tally: Http1ListTally, head: u8[], from: i32, to: i32, word: string): void => {
648
+ let start: i32 = from
649
+ for (let k: i32 = from; k <= to; k += 1) {
650
+ const c: i32 = k < to && k >= 0 && k < toI32(head.length) ? toI32(head[k]) : 44
651
+ if (c === 44) {
652
+ let a: i32 = start
653
+ let b: i32 = k
654
+ while (a >= 0 && a < b && a < toI32(head.length) && http1IsOws(toI32(head[a]))) {
655
+ a += 1
656
+ }
657
+ while (b > a && b - 1 >= 0 && b - 1 < toI32(head.length) && http1IsOws(toI32(head[b - 1]))) {
658
+ b -= 1
659
+ }
660
+ if (b > a) {
661
+ tally.elements += 1
662
+ tally.lastMatches = http1HeadIs(head, a, b, word, true)
663
+ if (tally.lastMatches) {
664
+ tally.matches += 1
665
+ }
666
+ }
667
+ start = k + 1
668
+ }
669
+ }
670
+ }
671
+
672
+ /**
673
+ * The list every field named `name` makes together (RFC 9110 §5.3: the
674
+ * fields of one name are one list), tallied for `word` into `p.tally`.
675
+ */
676
+ const http1Tally = (p: Http1Parser, name: string, word: string): Http1ListTally => {
677
+ const tally: Http1ListTally = p.tally
678
+ tally.elements = 0
679
+ tally.matches = 0
680
+ tally.lastMatches = false
681
+ const spans: i32[] = p.spans
682
+ for (let i: i32 = 0; i < p.headerCount && 4 * i + 3 < toI32(spans.length); i += 1) {
683
+ if (http1HeadIs(p.head, spans[4 * i], spans[4 * i + 1], name, false)) {
684
+ http1TallyRun(tally, p.head, spans[4 * i + 2], spans[4 * i + 3], word)
685
+ }
686
+ }
687
+ return tally
688
+ }
689
+
690
+ /**
691
+ * Whether the buffered start of a request line holds more than
692
+ * `HTTP1_MAX_METHOD` bytes before its first space (or line end).
693
+ */
694
+ const http1MethodOverrun = (p: Http1Parser): boolean => {
695
+ // Bytes up to `scan` were seen by an earlier call, which found a space
696
+ // in time if there were more than `HTTP1_MAX_METHOD` of them.
697
+ if (p.scan - p.start > HTTP1_MAX_METHOD) {
698
+ return false
699
+ }
700
+ const buf: u8[] = p.buf
701
+ const n: i32 = toI32(buf.length)
702
+ const stop: i32 = p.start + HTTP1_MAX_METHOD
703
+ for (let k: i32 = p.start; k >= 0 && k < p.end && k < n; k += 1) {
704
+ if (buf[k] === 32 || buf[k] === 13 || buf[k] === 10) {
705
+ return false
706
+ }
707
+ if (k >= stop) {
708
+ return true
709
+ }
710
+ }
711
+ return false
712
+ }
713
+
714
+ /**
715
+ * Reads the request line in `lineStart .. lineEnd`: method SP target SP
716
+ * version (§3), with exactly one space at each gap.
717
+ */
718
+ const http1RequestLine = (p: Http1Parser): i32 => {
719
+ const buf: u8[] = p.buf
720
+ const n: i32 = toI32(buf.length)
721
+ const a: i32 = p.lineStart
722
+ const b: i32 = p.lineEnd < n ? p.lineEnd : n
723
+ let i: i32 = a
724
+ while (i >= 0 && i < b && i < toI32(buf.length) && http1IsTokenByte(toI32(buf[i]))) {
725
+ i += 1
726
+ }
727
+ if (i === a || i - a > HTTP1_MAX_METHOD || i >= b || buf[i] !== 32) {
728
+ return http1Fail(p, 400, "a malformed method")
729
+ }
730
+ const methodEnd: i32 = i
731
+ i += 1
732
+ const targetStart: i32 = i
733
+ while (i >= 0 && i < b && i < toI32(buf.length) && buf[i] > 32 && buf[i] < 127) {
734
+ i += 1
735
+ }
736
+ if (i - targetStart > p.maxTarget) {
737
+ return http1Fail(p, 414, "the request target is over the limit")
738
+ }
739
+ if (i === targetStart || i >= b || buf[i] !== 32) {
740
+ return http1Fail(p, 400, "a malformed request target")
741
+ }
742
+ const targetEnd: i32 = i
743
+ i += 1
744
+ // "HTTP/" DIGIT "." DIGIT, case-sensitive (§2.3), and the end of the line.
745
+ if (
746
+ b - i !== 8 ||
747
+ buf[i] !== 72 ||
748
+ buf[i + 1] !== 84 ||
749
+ buf[i + 2] !== 84 ||
750
+ buf[i + 3] !== 80 ||
751
+ buf[i + 4] !== 47 ||
752
+ buf[i + 5] < 48 ||
753
+ buf[i + 5] > 57 ||
754
+ buf[i + 6] !== 46 ||
755
+ buf[i + 7] < 48 ||
756
+ buf[i + 7] > 57
757
+ ) {
758
+ return http1Fail(p, 400, "a malformed HTTP version")
759
+ }
760
+ if (buf[i + 5] !== 49) {
761
+ return http1Fail(p, 505, "an HTTP major version other than 1")
762
+ }
763
+ p.minor = buf[i + 7] === 48 ? 0 : 1
764
+ const at: i32 = http1HeadAppend(p, a, b)
765
+ p.methodStart = at
766
+ p.methodEnd = at + (methodEnd - a)
767
+ p.targetStart = at + (targetStart - a)
768
+ p.targetEnd = at + (targetEnd - a)
769
+ if (p.keepText) {
770
+ p.method = http1Text(buf, a, methodEnd, false)
771
+ p.target = http1Text(buf, targetStart, targetEnd, false)
772
+ }
773
+ p.state = HTTP1_S_FIELDS
774
+ return HTTP1_NEED_MORE
775
+ }
776
+
777
+ /**
778
+ * Reads one field line in `lineStart .. lineEnd` (§5) into `names` and
779
+ * `values`, or, for a trailer, only checks it. Answers `HTTP1_NEED_MORE` or the
780
+ * refusal.
781
+ */
782
+ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
783
+ const buf: u8[] = p.buf
784
+ const n: i32 = toI32(buf.length)
785
+ const a: i32 = p.lineStart
786
+ const b: i32 = p.lineEnd < n ? p.lineEnd : n
787
+ if (a >= 0 && a < b && (buf[a] === 32 || buf[a] === 9)) {
788
+ return http1Fail(p, 400, "a field line that starts with whitespace (obs-fold)")
789
+ }
790
+ let i: i32 = a
791
+ while (i >= 0 && i < b && i < toI32(buf.length) && http1IsTokenByte(toI32(buf[i]))) {
792
+ i += 1
793
+ }
794
+ if (i === a || i >= b || buf[i] !== 58) {
795
+ return http1Fail(p, 400, "a malformed field name")
796
+ }
797
+ const nameEnd: i32 = i
798
+ for (let k: i32 = nameEnd + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
799
+ if (!http1IsValueByte(toI32(buf[k]))) {
800
+ return http1Fail(p, 400, "a control character in a field value")
801
+ }
802
+ }
803
+ p.fieldCount += 1
804
+ if (p.fieldCount > p.maxHeaders) {
805
+ return http1Fail(p, 431, "more fields than the limit")
806
+ }
807
+ // A trailer is checked and counted, but not kept: nothing here reads one.
808
+ if (keep) {
809
+ let valueStart: i32 = nameEnd + 1
810
+ let valueEnd: i32 = b
811
+ while (valueStart >= 0 && valueStart < valueEnd && valueStart < n && http1IsOws(toI32(buf[valueStart]))) {
812
+ valueStart += 1
813
+ }
814
+ while (valueEnd > valueStart && valueEnd - 1 >= 0 && http1IsOws(toI32(buf[valueEnd - 1]))) {
815
+ valueEnd -= 1
816
+ }
817
+ const at: i32 = http1HeadAppend(p, a, b)
818
+ const head: u8[] = p.head
819
+ for (let k: i32 = at; k < at + (nameEnd - a) && k < toI32(head.length); k += 1) {
820
+ if (k >= 0 && head[k] >= 65 && head[k] <= 90) {
821
+ head[k] = head[k] + 32
822
+ }
823
+ }
824
+ const base: i32 = 4 * p.headerCount
825
+ http1SpanSet(p, base + 3, at + (valueEnd - a))
826
+ http1SpanSet(p, base, at)
827
+ http1SpanSet(p, base + 1, at + (nameEnd - a))
828
+ http1SpanSet(p, base + 2, at + (valueStart - a))
829
+ p.headerCount += 1
830
+ if (p.keepText) {
831
+ p.names.push(http1Text(buf, a, nameEnd, true))
832
+ p.values.push(http1Text(buf, valueStart, valueEnd, false))
833
+ }
834
+ }
835
+ return HTTP1_NEED_MORE
836
+ }
837
+
838
+ /**
839
+ * The header section has ended: decide the framing and the connection's fate
840
+ * (§6.3, §9.3) and answer `HTTP1_HEAD`, or the refusal. It reads the spans,
841
+ * and allocates nothing unless `keepText` asks for `upgrade` as a string.
842
+ */
843
+ const http1EndOfHead = (p: Http1Parser): i32 => {
844
+ const spans: i32[] = p.spans
845
+ const head: u8[] = p.head
846
+ let hosts: i32 = 0
847
+ let lengths: i32 = 0
848
+ let lengthField: i32 = -1
849
+ let codingFields: i32 = 0
850
+ let upgradeFields: i32 = 0
851
+ let upgradeBytes: i32 = 0
852
+ let connectionFields: i32 = 0
853
+ for (let i: i32 = 0; i < p.headerCount && 4 * i + 3 < toI32(spans.length); i += 1) {
854
+ const a: i32 = spans[4 * i]
855
+ const b: i32 = spans[4 * i + 1]
856
+ if (http1HeadIs(head, a, b, "host", false)) {
857
+ hosts += 1
858
+ } else if (http1HeadIs(head, a, b, "content-length", false)) {
859
+ lengths += 1
860
+ lengthField = i
861
+ } else if (http1HeadIs(head, a, b, "transfer-encoding", false)) {
862
+ codingFields += 1
863
+ } else if (http1HeadIs(head, a, b, "connection", false)) {
864
+ connectionFields += 1
865
+ } else if (http1HeadIs(head, a, b, "upgrade", false)) {
866
+ upgradeFields += 1
867
+ upgradeBytes += spans[4 * i + 3] - spans[4 * i + 2]
868
+ }
869
+ }
870
+ if ((p.minor === 1 && hosts !== 1) || hosts > 1) {
871
+ return http1Fail(p, 400, "a request without exactly one Host")
872
+ }
873
+ if (codingFields > 0) {
874
+ if (lengths > 0) {
875
+ return http1Fail(p, 400, "both Content-Length and Transfer-Encoding")
876
+ }
877
+ if (p.minor === 0) {
878
+ return http1Fail(p, 400, "Transfer-Encoding in an HTTP/1.0 request")
879
+ }
880
+ const codings: Http1ListTally = http1Tally(p, "transfer-encoding", "chunked")
881
+ if (!codings.lastMatches) {
882
+ return http1Fail(p, 400, "a transfer coding list that does not end in chunked")
883
+ }
884
+ if (codings.matches > 1) {
885
+ return http1Fail(p, 400, "chunked applied twice")
886
+ }
887
+ if (codings.elements > 1) {
888
+ return http1Fail(p, 501, "a transfer coding other than chunked")
889
+ }
890
+ p.chunked = true
891
+ } else if (lengths > 1) {
892
+ return http1Fail(p, 400, "Content-Length given twice")
893
+ } else if (lengths === 1 && 4 * lengthField + 3 < toI32(spans.length) && lengthField >= 0) {
894
+ const size: i32 = http1ContentLength(
895
+ head,
896
+ spans[4 * lengthField + 2],
897
+ spans[4 * lengthField + 3],
898
+ p.maxBody
899
+ )
900
+ if (size === -1) {
901
+ return http1Fail(p, 400, "a Content-Length that is not a run of digits")
902
+ }
903
+ if (size === -2) {
904
+ return http1Fail(p, 413, "a Content-Length over the limit")
905
+ }
906
+ p.contentLength = size
907
+ }
908
+ // Each tally is read before the next one reuses it, and none is walked
909
+ // for a request without a Connection field, which is most of them.
910
+ const listed: boolean = connectionFields > 0
911
+ const close: boolean = listed && http1Tally(p, "connection", "close").matches > 0
912
+ const keep: boolean = listed && http1Tally(p, "connection", "keep-alive").matches > 0
913
+ const upgradeAsked: boolean = listed && http1Tally(p, "connection", "upgrade").matches > 0
914
+ p.keepAlive = p.minor === 1 ? !close : keep && !close
915
+ // The fields of one name joined with ", " are empty only when there is one
916
+ // field and it is empty: the same test, without the join.
917
+ p.upgrading = p.minor === 1 && upgradeAsked && (upgradeFields > 1 || upgradeBytes > 0)
918
+ if (p.keepText && p.upgrading) {
919
+ const upgrades: string[] = []
920
+ const names: string[] = p.names
921
+ for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
922
+ if (names[i] === "upgrade") {
923
+ upgrades.push(p.values[i])
924
+ }
925
+ }
926
+ p.upgrade = upgrades.join(", ")
927
+ }
928
+ if (p.chunked) {
929
+ p.state = HTTP1_S_CHUNK_SIZE
930
+ } else if (p.contentLength > 0) {
931
+ p.left = p.contentLength
932
+ p.state = HTTP1_S_LENGTH_BODY
933
+ } else {
934
+ p.state = HTTP1_S_MESSAGE_END
935
+ }
936
+ return HTTP1_HEAD
937
+ }
938
+
939
+ /**
940
+ * `bytes[from .. to)` as a Content-Length (1*DIGIT, RFC 9112 §6.2): the
941
+ * value, -1 when it is not a run of digits, or -2 when it is one over
942
+ * `limit`. The digits are all read even past the limit, so `12x` is a 400
943
+ * however long it is.
944
+ */
945
+ const http1ContentLength = (bytes: u8[], from: i32, to: i32, limit: i32): i32 => {
946
+ if (to <= from) {
947
+ return -1
948
+ }
949
+ let value: i32 = 0
950
+ let over: boolean = false
951
+ for (let k: i32 = from; k < to && k >= 0 && k < toI32(bytes.length); k += 1) {
952
+ const c: i32 = toI32(bytes[k])
953
+ if (c < 48 || c > 57) {
954
+ return -1
955
+ }
956
+ if (!over) {
957
+ // `limit - digit` may be negative, and its division truncates toward
958
+ // zero, so the digit is compared on its own first.
959
+ if (c - 48 > limit || value > (limit - (c - 48)) / 10) {
960
+ over = true
961
+ } else {
962
+ value = value * 10 + (c - 48)
963
+ }
964
+ }
965
+ }
966
+ return over ? -2 : value
967
+ }
968
+
969
+ /** The value of the hex digit `c`, or -1. */
970
+ const http1HexValue = (c: i32): i32 => {
971
+ if (c >= 48 && c <= 57) {
972
+ return c - 48
973
+ }
974
+ if (c >= 97 && c <= 102) {
975
+ return c - 87
976
+ }
977
+ if (c >= 65 && c <= 70) {
978
+ return c - 55
979
+ }
980
+ return -1
981
+ }
982
+
983
+ /**
984
+ * Reads a chunk-size line (§7.1): 1*HEXDIG, then optional whitespace and a
985
+ * `;` extension that is read past but not interpreted. Sets `left` and answers
986
+ * `HTTP1_NEED_MORE`, or the refusal.
987
+ */
988
+ const http1ChunkSize = (p: Http1Parser): i32 => {
989
+ const buf: u8[] = p.buf
990
+ const n: i32 = toI32(buf.length)
991
+ const a: i32 = p.lineStart
992
+ const b: i32 = p.lineEnd < n ? p.lineEnd : n
993
+ const room: i32 = p.maxBody - p.bodyTotal
994
+ let size: i32 = 0
995
+ let over: boolean = false
996
+ let i: i32 = a
997
+ while (i >= 0 && i < b && i < toI32(buf.length)) {
998
+ const digit: i32 = http1HexValue(toI32(buf[i]))
999
+ if (digit < 0) {
1000
+ break
1001
+ }
1002
+ if (!over) {
1003
+ if (digit > room || size > (room - digit) / 16) {
1004
+ over = true
1005
+ } else {
1006
+ size = size * 16 + digit
1007
+ }
1008
+ }
1009
+ i += 1
1010
+ }
1011
+ if (i === a) {
1012
+ return http1Fail(p, 400, "a chunk size with no hex digits")
1013
+ }
1014
+ // Whitespace may follow the size only as the BWS before a chunk extension's
1015
+ // `;` (§7.1), so whitespace that runs to the end of the line is refused.
1016
+ const digitsEnd: i32 = i
1017
+ while (i >= 0 && i < b && i < toI32(buf.length) && http1IsOws(toI32(buf[i]))) {
1018
+ i += 1
1019
+ }
1020
+ if (i >= b && i > digitsEnd) {
1021
+ return http1Fail(p, 400, "a malformed chunk size")
1022
+ }
1023
+ if (i >= 0 && i < b) {
1024
+ if (buf[i] !== 59) {
1025
+ return http1Fail(p, 400, "a malformed chunk size")
1026
+ }
1027
+ for (let k: i32 = i + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
1028
+ if (!http1IsValueByte(toI32(buf[k]))) {
1029
+ return http1Fail(p, 400, "a control character in a chunk extension")
1030
+ }
1031
+ }
1032
+ }
1033
+ if (over) {
1034
+ return http1Fail(p, 413, "a chunked body over the limit")
1035
+ }
1036
+ p.left = size
1037
+ p.state = size === 0 ? HTTP1_S_TRAILERS : HTTP1_S_CHUNK_DATA
1038
+ p.sectionBytes = 0
1039
+ return HTTP1_NEED_MORE
1040
+ }
1041
+
1042
+ /**
1043
+ * Hands the caller up to `p.left` of the buffered body bytes, and moves to
1044
+ * `after` once the last of them is handed out.
1045
+ */
1046
+ const http1BodyWindow = (p: Http1Parser, after: i32): i32 => {
1047
+ const ready: i32 = p.end - p.start
1048
+ if (ready <= 0) {
1049
+ return HTTP1_NEED_MORE
1050
+ }
1051
+ let take: i32 = ready < p.left ? ready : p.left
1052
+ if (take > p.maxChunk && p.maxChunk > 0) {
1053
+ take = p.maxChunk
1054
+ }
1055
+ p.body = p.buf
1056
+ p.bodyOff = p.start
1057
+ p.bodyLen = take
1058
+ p.start += take
1059
+ p.scan = p.start
1060
+ p.left -= take
1061
+ p.bodyTotal += take
1062
+ if (p.left === 0) {
1063
+ p.state = after
1064
+ }
1065
+ return HTTP1_BODY
1066
+ }
1067
+
1068
+ /** The state machine behind `next()`. */
1069
+ const http1Next = (p: Http1Parser): i32 => {
1070
+ while (true) {
1071
+ switch (p.state) {
1072
+ case HTTP1_S_REQUEST_LINE: {
1073
+ // A method too long is a 400, decided before the line's length can
1074
+ // call it a long target and answer 414.
1075
+ if (http1MethodOverrun(p)) {
1076
+ return http1Fail(p, 400, "a malformed method")
1077
+ }
1078
+ // The longest well-formed request line: the method, the target, two
1079
+ // spaces and "HTTP/1.1".
1080
+ const got: i32 = http1TakeLine(p, HTTP1_MAX_METHOD + p.maxTarget + 10, 414)
1081
+ if (got !== HTTP1_LINE) {
1082
+ return got
1083
+ }
1084
+ // RFC 9112 §2.2: an empty line before a request line is ignored.
1085
+ if (p.lineEnd > p.lineStart && http1RequestLine(p) === HTTP1_ERROR) {
1086
+ return HTTP1_ERROR
1087
+ }
1088
+ break
1089
+ }
1090
+ case HTTP1_S_FIELDS:
1091
+ case HTTP1_S_TRAILERS: {
1092
+ const got: i32 = http1TakeLine(p, p.maxHeaderBytes - p.sectionBytes, 431)
1093
+ if (got !== HTTP1_LINE) {
1094
+ return got
1095
+ }
1096
+ p.sectionBytes += p.lineEnd - p.lineStart + 2
1097
+ if (p.sectionBytes > p.maxHeaderBytes) {
1098
+ return http1Fail(p, 431, "a header section over the limit")
1099
+ }
1100
+ const trailer: boolean = p.state === HTTP1_S_TRAILERS
1101
+ if (p.lineEnd === p.lineStart) {
1102
+ if (trailer) {
1103
+ p.state = HTTP1_S_MESSAGE_END
1104
+ break
1105
+ }
1106
+ return http1EndOfHead(p)
1107
+ }
1108
+ if (http1FieldLine(p, !trailer) === HTTP1_ERROR) {
1109
+ return HTTP1_ERROR
1110
+ }
1111
+ break
1112
+ }
1113
+ case HTTP1_S_LENGTH_BODY:
1114
+ return http1BodyWindow(p, HTTP1_S_MESSAGE_END)
1115
+ case HTTP1_S_CHUNK_SIZE: {
1116
+ const got: i32 = http1TakeLine(p, p.maxHeaderBytes, 400)
1117
+ if (got !== HTTP1_LINE) {
1118
+ return got
1119
+ }
1120
+ if (http1ChunkSize(p) === HTTP1_ERROR) {
1121
+ return HTTP1_ERROR
1122
+ }
1123
+ break
1124
+ }
1125
+ case HTTP1_S_CHUNK_DATA:
1126
+ return http1BodyWindow(p, HTTP1_S_CHUNK_CRLF)
1127
+ case HTTP1_S_CHUNK_CRLF: {
1128
+ // The CRLF after chunk data is an empty line, so a limit of zero
1129
+ // refuses anything else in its place with 400.
1130
+ const got: i32 = http1TakeLine(p, 0, 400)
1131
+ if (got !== HTTP1_LINE) {
1132
+ return got
1133
+ }
1134
+ p.state = HTTP1_S_CHUNK_SIZE
1135
+ break
1136
+ }
1137
+ case HTTP1_S_MESSAGE_END: {
1138
+ if (p.upgrading) {
1139
+ p.state = HTTP1_S_UPGRADED
1140
+ } else if (!p.keepAlive) {
1141
+ p.state = HTTP1_S_CLOSED
1142
+ } else {
1143
+ http1StartRequest(p)
1144
+ }
1145
+ return HTTP1_END
1146
+ }
1147
+ case HTTP1_S_UPGRADED:
1148
+ return HTTP1_UPGRADE
1149
+ case HTTP1_S_CLOSED:
1150
+ return HTTP1_CLOSED
1151
+ default:
1152
+ return HTTP1_ERROR
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // --- The writer -------------------------------------------------------------
1158
+
1159
+ /** The bytes of `text`, as the language holds a string. */
1160
+ const http1BytesOf = (text: string): u8[] => {
1161
+ const n: i32 = toI32(text.length)
1162
+ const out: u8[] = new Array<u8>(n)
1163
+ const outLength: i32 = toI32(out.length)
1164
+ for (let k: i32 = 0; k < outLength && k < n; k += 1) {
1165
+ out[k] = toU8(text.charCodeAt(k))
1166
+ }
1167
+ return out
1168
+ }
1169
+
1170
+ /** Whether every byte of `text` is a tchar, and there is at least one. */
1171
+ const http1IsToken = (text: string): boolean => {
1172
+ const n: i32 = toI32(text.length)
1173
+ if (n === 0) {
1174
+ return false
1175
+ }
1176
+ for (let k: i32 = 0; k < n; k += 1) {
1177
+ if (!http1IsTokenByte(toI32(text.charCodeAt(k)))) {
1178
+ return false
1179
+ }
1180
+ }
1181
+ return true
1182
+ }
1183
+
1184
+ /** Whether every byte of `text` may stand in a field value or a reason phrase. */
1185
+ const http1IsFieldText = (text: string): boolean => {
1186
+ const n: i32 = toI32(text.length)
1187
+ for (let k: i32 = 0; k < n; k += 1) {
1188
+ if (!http1IsValueByte(toI32(text.charCodeAt(k)))) {
1189
+ return false
1190
+ }
1191
+ }
1192
+ return true
1193
+ }
1194
+
1195
+ /**
1196
+ * An HTTP/1.1 status line and header section (RFC 9112 §4, §5), ending in
1197
+ * the empty line, with the framing field `bodyLength` asks for appended:
1198
+ * `Content-Length: n` for `n >= 0`, `Transfer-Encoding: chunked` for
1199
+ * `HTTP1_CHUNKED`, and neither for `HTTP1_NO_BODY`.
1200
+ *
1201
+ * Answers `null`, and writes nothing, for a status outside 100 to 599; a
1202
+ * reason phrase or field value holding CR, LF or another control character
1203
+ * (which would end the line early and let the rest be read as a field or a
1204
+ * second response); a field name that is not a token; `names` and `values` of
1205
+ * different lengths; a `Content-Length` or `Transfer-Encoding` among the
1206
+ * caller's fields, since the framing is decided here; a 1xx or 204 response
1207
+ * with a body length (RFC 9110 §15.2, §15.3.5); and any other `bodyLength`
1208
+ * below zero.
1209
+ */
1210
+ export const http1ResponseHead = (
1211
+ status: i32,
1212
+ reason: string,
1213
+ names: string[],
1214
+ values: string[],
1215
+ bodyLength: i32
1216
+ ): u8[] | null => {
1217
+ if (status < 100 || status > 599 || !http1IsFieldText(reason)) {
1218
+ return null
1219
+ }
1220
+ if (bodyLength < HTTP1_NO_BODY) {
1221
+ return null
1222
+ }
1223
+ if ((status < 200 || status === 204) && bodyLength !== HTTP1_NO_BODY) {
1224
+ return null
1225
+ }
1226
+ const count: i32 = toI32(names.length)
1227
+ if (toI32(values.length) !== count) {
1228
+ return null
1229
+ }
1230
+ const parts: string[] = [`HTTP/1.1 ${status} ${reason}\r\n`]
1231
+ for (let i: i32 = 0; i < count && i < toI32(names.length) && i < toI32(values.length); i += 1) {
1232
+ const name: string = names[i]
1233
+ const value: string = values[i]
1234
+ const nameLength: i32 = toI32(name.length)
1235
+ if (!http1IsToken(name) || !http1IsFieldText(value)) {
1236
+ return null
1237
+ }
1238
+ if (
1239
+ http1SameWord(name, 0, nameLength, "content-length") ||
1240
+ http1SameWord(name, 0, nameLength, "transfer-encoding")
1241
+ ) {
1242
+ return null
1243
+ }
1244
+ parts.push(`${name}: ${value}\r\n`)
1245
+ }
1246
+ if (bodyLength >= 0) {
1247
+ parts.push(`Content-Length: ${bodyLength}\r\n`)
1248
+ } else if (bodyLength === HTTP1_CHUNKED) {
1249
+ parts.push("Transfer-Encoding: chunked\r\n")
1250
+ }
1251
+ parts.push("\r\n")
1252
+ return http1BytesOf(parts.join(""))
1253
+ }
1254
+
1255
+ /** How many hex digits `len` takes, without leading zeros: at least one. */
1256
+ const http1HexDigits = (len: i32): i32 => {
1257
+ let digits: i32 = 1
1258
+ while (digits < 8 && len >> (4 * digits) !== 0) {
1259
+ digits += 1
1260
+ }
1261
+ return digits
1262
+ }
1263
+
1264
+ /**
1265
+ * Writes `data[off .. off + len)` as one chunk of a chunked body (RFC 9112
1266
+ * §7.1) into `out` at `at` — the size in lowercase hex, CRLF, the bytes,
1267
+ * CRLF — and answers where it ends, or `HTTP1_NO_ROOM` when it does not fit,
1268
+ * with `out` untouched. An empty window writes nothing and answers `at`,
1269
+ * because a chunk of size zero ends the body. A window outside `data` panics.
1270
+ */
1271
+ export const http1WriteChunk = (out: u8[], at: i32, data: u8[], off: i32, len: i32): i32 => {
1272
+ const size: i32 = toI32(data.length)
1273
+ if (off < 0 || len < 0 || off > size || len > size - off) {
1274
+ panic("http1WriteChunk: the window is outside the buffer")
1275
+ }
1276
+ if (len === 0) {
1277
+ return at
1278
+ }
1279
+ const digits: i32 = http1HexDigits(len)
1280
+ const outLength: i32 = toI32(out.length)
1281
+ if (at < 0 || at > outLength || len > outLength - at - digits - 4) {
1282
+ return HTTP1_NO_ROOM
1283
+ }
1284
+ let v: i32 = len
1285
+ for (let k: i32 = at + digits - 1; k >= at && k >= 0 && k < outLength; k -= 1) {
1286
+ const d: i32 = v & 15
1287
+ out[k] = toU8(d < 10 ? 48 + d : 87 + d)
1288
+ v = v >> 4
1289
+ }
1290
+ const headEnd: i32 = at + digits + 2
1291
+ out[headEnd - 2] = 13
1292
+ out[headEnd - 1] = 10
1293
+ for (let k: i32 = 0; k < len && headEnd + k < outLength; k += 1) {
1294
+ out[headEnd + k] = data[off + k]
1295
+ }
1296
+ out[headEnd + len] = 13
1297
+ out[headEnd + len + 1] = 10
1298
+ return headEnd + len + 2
1299
+ }
1300
+
1301
+ /**
1302
+ * `data[off .. off + len)` as one chunk of a chunked body, in an array of its
1303
+ * own; `http1WriteChunk` writes the same bytes into a buffer the caller owns.
1304
+ * An empty window answers an empty array, because a chunk of size zero is the
1305
+ * end of the body and only `http1LastChunk` writes that. A window outside
1306
+ * `data` panics.
1307
+ */
1308
+ export const http1Chunk = (data: u8[], off: i32, len: i32): u8[] => {
1309
+ const size: i32 = toI32(data.length)
1310
+ if (off < 0 || len < 0 || off > size || len > size - off) {
1311
+ panic("http1Chunk: the window is outside the buffer")
1312
+ }
1313
+ if (len === 0) {
1314
+ return []
1315
+ }
1316
+ const out: u8[] = new Array<u8>(http1HexDigits(len) + len + 4)
1317
+ http1WriteChunk(out, 0, data, off, len)
1318
+ return out
1319
+ }
1320
+
1321
+ /** The last chunk and the empty trailer section that end a chunked body: `0\r\n\r\n`. */
1322
+ export const http1LastChunk = (): u8[] => [48, 13, 10, 13, 10]
1323
+
1324
+ /**
1325
+ * Writes `http1LastChunk`'s five bytes into `out` at `at` and answers where
1326
+ * they end, or `HTTP1_NO_ROOM` with `out` untouched.
1327
+ */
1328
+ export const http1WriteLastChunk = (out: u8[], at: i32): i32 => {
1329
+ if (at < 0 || at > toI32(out.length) - 5) {
1330
+ return HTTP1_NO_ROOM
1331
+ }
1332
+ return http1PutText(out, at, "0\r\n\r\n")
1333
+ }
1334
+
1335
+ /** `http1WriteResponseHead` and `http1WriteChunk`: the bytes do not fit at `at`. */
1336
+ export const HTTP1_NO_ROOM: i32 = -1
1337
+ /** `http1WriteResponseHead`: a head the writer refuses to write. */
1338
+ export const HTTP1_REFUSED: i32 = -2
1339
+
1340
+ /** Whether `bytes` is a non-empty run of tchars. */
1341
+ const http1BytesAreToken = (bytes: u8[]): boolean => {
1342
+ for (const b of bytes) {
1343
+ if (!http1IsTokenByte(toI32(b))) {
1344
+ return false
1345
+ }
1346
+ }
1347
+ return toI32(bytes.length) > 0
1348
+ }
1349
+
1350
+ /** Whether every byte of `bytes` may stand in a field value. */
1351
+ const http1BytesAreFieldText = (bytes: u8[]): boolean => {
1352
+ for (const b of bytes) {
1353
+ if (!http1IsValueByte(toI32(b))) {
1354
+ return false
1355
+ }
1356
+ }
1357
+ return true
1358
+ }
1359
+
1360
+ /** Whether the octets of `bytes` spell `word`, which is lowercase, in any ASCII case: a field name compared as RFC 9110 §5.1 says. */
1361
+ export const http1BytesAre = (bytes: u8[], word: string): boolean =>
1362
+ http1HeadIs(bytes, 0, toI32(bytes.length), word, true)
1363
+
1364
+ /** How many decimal digits `n`, which is not negative, takes. */
1365
+ const http1DecimalDigits = (n: i32): i32 => {
1366
+ let digits: i32 = 1
1367
+ let v: i32 = n
1368
+ while (v >= 10) {
1369
+ v = v / 10
1370
+ digits += 1
1371
+ }
1372
+ return digits
1373
+ }
1374
+
1375
+ /** Writes `text` at `out[at ..)`, which the caller has made sure holds it, and answers where it ends. */
1376
+ const http1PutText = (out: u8[], at: i32, text: string): i32 => {
1377
+ const n: i32 = toI32(text.length)
1378
+ for (let k: i32 = 0; k < n && at + k < toI32(out.length); k += 1) {
1379
+ if (at + k >= 0) {
1380
+ out[at + k] = toU8(text.charCodeAt(k))
1381
+ }
1382
+ }
1383
+ return at + n
1384
+ }
1385
+
1386
+ /** Writes `bytes` at `out[at ..)`, likewise. */
1387
+ const http1PutBytes = (out: u8[], at: i32, bytes: u8[]): i32 => {
1388
+ const n: i32 = toI32(bytes.length)
1389
+ for (let k: i32 = 0; k < n && at + k < toI32(out.length); k += 1) {
1390
+ if (at + k >= 0) {
1391
+ out[at + k] = bytes[k]
1392
+ }
1393
+ }
1394
+ return at + n
1395
+ }
1396
+
1397
+ /** Writes the decimal digits of `n`, which is not negative, at `out[at ..)`, likewise. */
1398
+ const http1PutDecimal = (out: u8[], at: i32, n: i32): i32 => {
1399
+ const digits: i32 = http1DecimalDigits(n)
1400
+ let v: i32 = n
1401
+ for (let k: i32 = at + digits - 1; k >= at && k >= 0 && k < toI32(out.length); k -= 1) {
1402
+ const digit: i32 = v % 10
1403
+ out[k] = toU8(48 + digit)
1404
+ v = v / 10
1405
+ }
1406
+ return at + digits
1407
+ }
1408
+
1409
+ /**
1410
+ * `http1ResponseHead`'s status line and header section, written into `out`
1411
+ * at `at` with nothing allocated: field names and values are octets (as
1412
+ * `nish/net/http-fields` holds them), and `connection`, when not empty, is
1413
+ * sent as a `Connection` field after the caller's. Answers where the head
1414
+ * ends; `HTTP1_REFUSED` for everything `http1ResponseHead` refuses, a
1415
+ * `Connection` among the caller's fields when `connection` is given, and a
1416
+ * `connection` that is not field text; `HTTP1_NO_ROOM` when it is well formed
1417
+ * but does not fit. Either way `out` is untouched.
1418
+ */
1419
+ export const http1WriteResponseHead = (
1420
+ out: u8[],
1421
+ at: i32,
1422
+ status: i32,
1423
+ reason: string,
1424
+ names: u8[][],
1425
+ values: u8[][],
1426
+ bodyLength: i32,
1427
+ connection: string
1428
+ ): i32 => {
1429
+ if (status < 100 || status > 599 || !http1IsFieldText(reason) || !http1IsFieldText(connection)) {
1430
+ return HTTP1_REFUSED
1431
+ }
1432
+ if (bodyLength < HTTP1_NO_BODY || ((status < 200 || status === 204) && bodyLength !== HTTP1_NO_BODY)) {
1433
+ return HTTP1_REFUSED
1434
+ }
1435
+ const count: i32 = toI32(names.length)
1436
+ if (toI32(values.length) !== count) {
1437
+ return HTTP1_REFUSED
1438
+ }
1439
+ // "HTTP/1.1 " status SP reason CRLF, then each field, then the framing
1440
+ // field, the connection field and the empty line: counted before anything
1441
+ // is written, in 64 bits so that no caller's lengths can wrap the sum.
1442
+ let need: i64 = toI64(17) + toI64(reason.length)
1443
+ for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
1444
+ const name: u8[] = names[i]
1445
+ const value: u8[] = values[i]
1446
+ if (!http1BytesAreToken(name) || !http1BytesAreFieldText(value)) {
1447
+ return HTTP1_REFUSED
1448
+ }
1449
+ if (http1BytesAre(name, "content-length") || http1BytesAre(name, "transfer-encoding")) {
1450
+ return HTTP1_REFUSED
1451
+ }
1452
+ if (toI32(connection.length) > 0 && http1BytesAre(name, "connection")) {
1453
+ return HTTP1_REFUSED
1454
+ }
1455
+ need = need + toI64(name.length) + toI64(value.length) + toI64(4)
1456
+ }
1457
+ if (bodyLength >= 0) {
1458
+ need = need + toI64(18 + http1DecimalDigits(bodyLength))
1459
+ } else if (bodyLength === HTTP1_CHUNKED) {
1460
+ need = need + toI64(28)
1461
+ }
1462
+ if (toI32(connection.length) > 0) {
1463
+ need = need + toI64(14) + toI64(connection.length)
1464
+ }
1465
+ if (at < 0 || at > toI32(out.length) || need > toI64(toI32(out.length) - at)) {
1466
+ return HTTP1_NO_ROOM
1467
+ }
1468
+ let end: i32 = http1PutText(out, at, "HTTP/1.1 ")
1469
+ end = http1PutDecimal(out, end, status)
1470
+ end = http1PutText(out, end, " ")
1471
+ end = http1PutText(out, end, reason)
1472
+ end = http1PutText(out, end, "\r\n")
1473
+ for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
1474
+ end = http1PutBytes(out, end, names[i])
1475
+ end = http1PutText(out, end, ": ")
1476
+ end = http1PutBytes(out, end, values[i])
1477
+ end = http1PutText(out, end, "\r\n")
1478
+ }
1479
+ if (bodyLength >= 0) {
1480
+ end = http1PutText(out, end, "Content-Length: ")
1481
+ end = http1PutDecimal(out, end, bodyLength)
1482
+ end = http1PutText(out, end, "\r\n")
1483
+ } else if (bodyLength === HTTP1_CHUNKED) {
1484
+ end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
1485
+ }
1486
+ if (toI32(connection.length) > 0) {
1487
+ end = http1PutText(out, end, "Connection: ")
1488
+ end = http1PutText(out, end, connection)
1489
+ end = http1PutText(out, end, "\r\n")
1490
+ }
1491
+ return http1PutText(out, end, "\r\n")
1492
+ }