@amritk/nish-x86_64-linux 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +43 -34
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +50 -13
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +43 -24
- package/runtime/runtime-net.c +57 -10
- package/runtime/runtime-os.c +62 -20
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +12 -0
- package/runtime/runtime.c +66 -18
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +14 -12
- package/std/README.md +168 -36
- package/std/crypto/aes.ts +51 -8
- package/std/crypto/base64url.ts +25 -12
- package/std/crypto/chacha20poly1305.ts +43 -29
- package/std/crypto/ct.ts +6 -1
- package/std/crypto/hkdf.ts +364 -3
- package/std/crypto/hmac.ts +307 -7
- package/std/crypto/p256.ts +618 -34
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +3 -2
- package/std/crypto/x25519.ts +88 -36
- package/std/crypto/x509.ts +130 -47
- package/std/json.ts +297 -41
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +911 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +462 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +929 -0
- package/std/net/quic-packet.ts +1698 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1609 -0
- package/std/net/quic.ts +3578 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +414 -0
- package/std/net/tls.ts +1437 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
- package/std/text.ts +77 -0
package/std/net/http1.ts
ADDED
|
@@ -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
|
+
}
|